汇率说明:本文出现的美元金额按 2026-10 基准汇率 1 USD ≈ 6.71 元换算,人民币后括号内标注原币种。
一、告警系统失败的方式,通常不是"没告警"
绝大多数物联网项目对告警的期待是"出事就响"。但真正把告警系统拖垮的,从来不是漏报,而是没人看的告警。
几组公开数据可以说明问题的规模:
| 数据 | 数值 | 来源 |
|---|---|---|
| 安全运营中心日均告警量 | 约 2,992 条/天,其中约 63% 未被处理 | Vectra AI 2026 |
| 同上(另一口径) | 约 2,047 条/天,其中 300 条(15%)因资源不足未被检查 | Binalyze《2026 State of the SOC》(Sapio 调研 400 名 CISO) |
| 信息过载的后果 | 91% 的 CISO 承认组织存在信息过载,77% 因此漏掉过告警或漏洞 | 同上 |
| 工业现场的误报率 | 传感器告警中 40%--90% 最终是误报(漂移、环境噪声、标定衰减) | 多份工业运维研究的一致区间 |
| 重复告警的代价 | 每重复一次同类告警,运维人员真正采取行动的概率下降约 30% | 工业告警管理研究 |
一个可以换算的参考:一家 180 台设备的工厂,若误报率 35%,每年约有 1,200 个工时消耗在"追假告警"上,折合约 28.2 万元(原报告 4.2 万美元)。
(需要说明的是,前两组来自安全运营领域,不是物联网领域。但机制是一样的:噪声越大,真正重要的那一条越容易被淹没。)
所以本文不讨论"怎么让告警更多",只讨论三件事:
- 不迟到------该响的时候,最迟在下一个上报周期内响;
- 不刷屏------同一件事只产生一条告警,恢复后自动消失;
- 找对人------紧急的进手机,不紧急的只进列表,别把两者混在一起。
二、LoRaWAN 场景下,先算清三笔时间账
在 LoRaWAN 上做告警,第一件事不是写脚本,是接受一个物理事实:电池供电的终端不会为了告警而多醒一次。由此决定了三条时间线:
| 环节 | 决定因素 | 典型量级 | 你能做的 |
|---|---|---|---|
| 检测延迟 | 终端上报周期(采集周期 + COV 变化上报) | 5 分钟 -- 60 分钟;COV 未触发时更长 | 缩短采集周期,或把 COV 阈值调小 |
| 下行延迟 | Class A 终端只在自己上行之后开窗(RX1 约 1 秒、RX2 约 2 秒,LoRaWAN 1.0.4 默认值);错过窗口就要等下一次上行 | 最好 1--2 秒,最坏 = 一个完整上报周期 | 关键回路用 Class C;否则把延迟写进 SLA |
| 失联判定 | 平台按"最后活动时间 + 2 × 心跳周期(heart_period)"判离线 | 由项目配置的心跳周期决定 | 心跳周期设成上报周期的 2--3 倍 |
这三笔账的意义在于:**LoRaWAN 告警做不到秒级,但完全可以做到"不迟过一个上报周期"**。把这个结论明明白白写进方案书,比事后被客户追问"为什么告警晚了 20 分钟"要好得多。
三、ThinkLink 的告警三件套
ThinkLink 的告警不是"在设备上配阈值",而是三个部件协作:
| 部件 | 职责 | 配置位置 |
|---|---|---|
| 触发模型 | 每次上行数据到达后执行脚本,判断条件,输出动作 | 模型管理 → 触发模型 → 挂载到设备 |
| 内置 ALARM RPC | 接收 action: new / clear,创建或清除告警记录,并按通知组分发出去 |
运维管理 → 设备详情 → RPC → 新增 ALARM |
| 通知组 | 决定"发给谁、通过什么渠道" | 告警管理 → 通知组 |
配置顺序固定为三步:先给设备挂 ALARM RPC,再挂触发模型,最后(可选)配通知组。三者缺一个,告警就是"哑"的------不存在"配了脚本就自动会响"这件事。
一个设备可以挂载多个触发模型,多条件场景不用挤在一个脚本里。
四、触发脚本的运行环境:写裸函数体,跑在 vm2 沙盒里
触发脚本有一个容易踩的细节:编辑器里写的是裸函数体 ,平台会把它包进 function triggerRunner() { ... } 再调用,所以不需要(也不应该)再套一层 function trigger_script(...) {}。文档里的 trigger_script 写法只是为了说明可用变量。
运行环境是 vm2 沙盒,限制如下:
| 限制项 | 值 |
|---|---|
| 执行超时 | 1000 ms,超时本次触发直接跳过 |
eval/ WebAssembly |
禁用 |
async/await |
不支持,脚本同步执行 |
require/import |
不支持 |
setTimeout/setInterval/console |
不可用;延时执行要用返回值的 delayms 字段,调试用触发模型日志 |
process/path/fs |
不可用 |
Buffer |
SafeBuffer,仅开放 alloc/from/isBuffer/byteLength/compare/concat |
可直接使用的注入变量:device、thingModelId、org_params(组织参数)、env、plugins、Buffer,以及 tklHelper 的六个类(BufferTKL、PayloadParser、MSparser、Utils、TriggerHelper、RPCHelper)。
device 对象里与告警最相关的字段:
| 字段 | 说明 |
|---|---|
device.telemetry_data[thingModelId] |
该物模型的最新遥测快照(取数入口) |
device.server_attrs |
平台侧属性,适合放阈值、通知组、联动 EUI |
device.shared_attrs |
与设备双向同步的属性 |
device.active_time |
最近一次上行时间(ISO 8601) |
device.heart_period |
心跳周期(秒),用于失联判断 |
device.eui/device.name |
设备标识与名称 |
注意:device 与 tklHelper 各类都以冻结方式注入,赋值不生效,脚本唯一的输出通道是返回值。
五、返回值四个字段,其中一个是平台自带的降噪开关
触发脚本返回 null(什么都不做)或一个包含下列字段的对象:
| 字段 | 类型 | 说明 |
|---|---|---|
delayms |
Number | 执行 actions 前的延迟(毫秒) |
abort_previous_timer |
Boolean | true 时取消上一次未执行的延迟任务,用本次替代 |
should_dispatch |
Boolean | **默认 false**:本次数据与上次相同时跳过下发。设为 true 则强制执行 |
actions |
Array | 按顺序执行的 RPC 调用数组 |
actions 每一项的字段:
| 字段 | 说明 |
|---|---|
method |
RPC 的 Method 名称(告警场景固定为 alarm) |
methodId |
与 method 二选一,重名时优先用 methodId |
params._eui |
目标设备 EUI,写在 params 里,不是 action 的顶层字段 |
**should_dispatch 默认 false 是平台送的第一层降噪**------重复上报相同数据时不会重复派发动作。很多人不知道这一点,又自己去写了一套去重逻辑。
六、一条可以照抄的告警脚本
下面是一个温度 + 电池双条件告警,阈值和通知组全部外置到 server_attrs,改阈值不用改脚本:
// 触发脚本写裸函数体,平台会包进 function triggerRunner(){...} let tdata = device?.telemetry_data?.thingModelId; if (!tdata) return null;
const LIMIT_TEMP = device.server_attrs?.alarm_temp ?? 60; const LIMIT_BAT = device.server_attrs?.alarm_battery ?? 20; const notify = device.server_attrs?.notify ?? \[\];
let actions = \[\];
// 1)过热:等级与标题保持固定,便于平台去重 let hot = tdata.temperature >= LIMIT_TEMP; actions.push({ method: "alarm", params: { _eui: device.eui, name: "overheat_alarm", title: "过热 " + device.name, level: "urgent", desc: "温度超过上限,请立即处理", action: hot ? "new" : "clear", notify: notify } });
// 2)低电量:只进列表,不打扰(notify 传空数组) let lowBat = tdata.battery <= LIMIT_BAT; actions.push({ method: "alarm", params: { _eui: device.eui, name: "low_battery_alarm", title: "低电量 " + device.name, level: "mid", desc: "电池电量低于阈值,建议排入更换计划", action: lowBat ? "new" : "clear", notify: \[\] } });
return { delayms: 0, abort_previous_timer: true, actions };
ALARM RPC 的参数说明:
| 参数 | 说明 |
|---|---|
name |
告警事件唯一标识,一种告警一个名字 |
action |
new 产生,clear 清除,no 不动作 |
title |
界面显示的标题 |
desc |
描述信息 |
level |
low/mid/high/urgent |
notify |
通知组名称数组,逐项分发;空数组 = 只记不送 |
配套写在设备 server_attrs 里的配置:
| 字段 | 示例值 | 作用 |
|---|---|---|
alarm_temp |
60 | 温度阈值 |
alarm_battery |
20 | 电量阈值 |
notify |
["ops-team"] |
通知组列表 |
如果不想手写返回对象,可以继承 TriggerHelper 并重写 _alarmProcess(info):构造函数会自动从 device.server_attrs.notify 读取通知组,并自动生成标准返回结构(其中参数名为 group,对应 ALARM RPC 的 notify)。_alarmProcess 返回 false 或 action 为 no 时,alarm() 返回 null。
七、四级告警,以及一张分级矩阵
告警等级只有四个:low(提示)、mid(警告)、high(错误)、urgent(紧急)。等级本身不决定任何行为,决定行为的是"等级 × 通知组"的组合:
| 场景 | 建议等级 | notify | 响应预期 |
|---|---|---|---|
| 水浸、可燃气体、机房高温 | urgent |
值班组(企业微信 WebHook) | 立即 |
| 越限但可容忍(温湿度偏差) | high |
运维组邮件 | 当班处理 |
| 电池低电、信号质量劣化 | mid |
空数组,只进列表 | 纳入计划 |
| 参数被改、固件版本不一致 | low |
空数组 | 巡检时看 |
这张表建议在项目启动会上跟客户对齐一次。告警分级不是技术问题,是责任划分问题------**每一级告警都要有一个明确的"谁负责"**,否则 urgent 也会变成没人看。
八、降噪六招
- 别关掉
should_dispatch的默认去重 。默认false意味着相同数据不重复派发,这是白送的一层降噪。只有在"每次都要强制下发"的场景才设true。 - 让
title、level、desc、name保持稳定 。平台对同名且标题、描述、等级完全相同的告警会做抑制,不重复创建。所以**不要把当前数值拼进desc**------温度 61.2 和 61.5 会被判成两条不同的告警,去重失效,一晚上刷出几十条。要写数值,就写区间或等级("温度超过上限")。 - **
clear默认化**。把action默认设成clear,只在条件满足时切成new。这样每次上行都是一次"恢复确认",条件正常了告警自动消失,不留僵尸告警。反过来(只在超限时发new),一旦设备离线,告警会永远挂着。 - 用
delayms做持续确认 。抖动型测点(门磁、水浸、瞬时越限)不要一超限就告警,可以返回delayms延后执行,配合abort_previous_timer: true,让"上一次还没执行的定时动作"被本次替代------只有持续越限的那一趟才能真正落地成告警。 - **阈值和通知组放
server_attrs**。改阈值不用改脚本、不用产生新版本,也不用重新挂载。多设备共享一个触发模型、各自配自己的阈值,这是规模化部署的前提。 - 多参数确认 。温度高且湿度同步变化,和温度单独跳一下,是两种可信度。单一参数越限进
mid,两个参数同时越限才升到high,能砍掉相当一部分误报。
九、通知组:能力边界如实说
通知组(告警管理 → 通知组)的必填项是 Id Name、名称、通知方式、联系人,标签和备注可选。
| 项 | 实际情况 |
|---|---|
| 内置通知方式 | 邮件 (默认)与企业微信 WebHook |
| 联系人 | 可多选 |
| 启用开关 | 默认关闭,配完必须手动打开 |
| 钉钉 / Slack / Telegram | 不在内置表单里,需要走自定义 RPC 接入 |
这一点值得单独强调:把钉钉、Slack、Telegram 写成"平台内置渠道"是不准确的。它们的接入路径是自定义 RPC------能实现,但不是开箱即用的勾选项。方案书上要按实际口径写,否则验收时会对不上。
实践建议:在设备 server_attrs 里以数组形式存通知组名(如 notify: ["ops-team"]),脚本读出来直接传给 notify。这样"分级路由"就变成了改属性,而不是改脚本------同一个触发模型挂 500 台设备,可以各走各的组。
十、三个可直接套用的配方
| 场景 | 触发条件 | 等级 | 通知 | 恢复方式 |
|---|---|---|---|---|
| KS52 水浸 | water_leak == 1 |
urgent |
值班组企业微信 | 数据恢复即 clear |
| 电池低电 | battery <= 20 |
mid |
空数组(只进列表) | 换电池后 clear |
| 设备失联 | active_time 超过 2 ×heart_period |
high |
运维组邮件 | 下次上行即 clear |
失联告警需要说明:平台对在线状态有三个不同的口径(列表与统计的活动时间口径、server_attrs.online 检测器、MQTT 的 data.online 通知),三者不能直接互换。用触发脚本做失联判断时,建议明确写清自己用的是 active_time 与 heart_period 这一路,并在文档里注明。
十一、上线前自查清单
- 目标设备已挂载
ALARMRPC(不是只挂了触发模型) - 触发模型已挂到设备的「触发」标签页(不是只创建了模型)
-
params里有_eui,且写在params里而不是 action 顶层 -
name唯一且稳定,同一类告警一个名字 -
level是四个合法值之一,缺失会导致告警不产生 - 阈值与通知组在
server_attrs里,server_attrs未配置时脚本有默认值 -
action默认是clear,不是只在超限时发new -
desc里没有拼当前数值(否则去重失效) - 通知组已手动启用(默认是关的)
- 用编辑器测试页签跑过一遍
new和clear两条路径
十二、八个常踩的坑
| 现象 | 原因 |
|---|---|
| 脚本完全不执行 | 触发模型没挂到设备上,只创建了模型 |
| 脚本执行了但没告警 | 设备上没挂 ALARM RPC |
| 告警刷屏 | desc 里拼了实时数值,去重被绕过 |
| 告警永远不消失 | 只发 new 不发 clear,设备离线后告警挂死 |
| 改了阈值没生效 | 阈值硬编码在脚本里,改的是 server_attrs |
脚本报 ReferenceError |
用了 console/setTimeout/process 等沙盒不注入的对象 |
| 误以为脚本卡住 | 用了 async/await,沙盒不支持异步 |
| 以为"全部已读"= 故障恢复 | 那只是阅读状态,与告警是否 clear、通知是否送达是三件事 |
结语
一套能用的告警系统,技术含量不在脚本本身,而在这三个判断:什么是真的异常、谁必须知道、什么时候闭嘴。ThinkLink 把这三个判断都交给了脚本和属性配置------好处是灵活(同一套模型可以服务 500 台阈值不同的设备),代价是需要有人在上线前把规则想清楚。
本文的六招降噪里,最容易被忽略的是第 2 条(保持 desc 稳定)和第 3 条(clear 默认化)。这两条不写,系统上线第一周可能一切正常,第二周开始就没人愿意看告警列表了。