汇率说明:本文出现的美元金额按 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 上做告警,第一件事不是写脚本,是接受一个物理事实:电池供电的终端不会为了告警而多醒一次。由此决定了三条时间线:
| 环节 | 决定因素 | 典型量级 | 你能做的 |
|---|---|---|---|
| 检测延迟 | 终端上报周期(采集周期 + 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 的告警不是"在设备上配阈值",而是三个部件协作:
| 部件 | 职责 | 配置位置 |
|---|---|---|
| 触发模型 | 每次上行数据到达后执行脚本,判断条件,输出动作 | 模型管理 → 触发模型 → 挂载到设备 |
| 内置 ALARM RPC | 接收 action: new / clear,创建或清除告警记录,并按通知组分发出去 |
运维管理 → 设备详情 → RPC → 新增 ALARM |
| 通知组 | 决定"发给谁、通过什么渠道" | 告警管理 → 通知组 |
配置顺序固定为三步:先给设备挂 ALARM RPC,再挂触发模型,最后(可选)配通知组。三者缺一个,告警就是"哑"的——不存在"配了脚本就自动会响"这件事。
一个设备可以挂载多个触发模型,多条件场景不用挤在一个脚本里。
触发脚本有一个容易踩的细节:编辑器里写的是裸函数体,平台会把它包进 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 这一路,并在文档里注明。
ALARM RPC(不是只挂了触发模型)params 里有 _eui,且写在 params 里而不是 action 顶层name 唯一且稳定,同一类告警一个名字level 是四个合法值之一,缺失会导致告警不产生server_attrs 里,server_attrs 未配置时脚本有默认值action 默认是 clear,不是只在超限时发 newdesc 里没有拼当前数值(否则去重失效)new 和 clear 两条路径| 现象 | 原因 |
|---|---|
| 脚本完全不执行 | 触发模型没挂到设备上,只创建了模型 |
| 脚本执行了但没告警 | 设备上没挂 ALARM RPC |
| 告警刷屏 | desc 里拼了实时数值,去重被绕过 |
| 告警永远不消失 | 只发 new 不发 clear,设备离线后告警挂死 |
| 改了阈值没生效 | 阈值硬编码在脚本里,改的是 server_attrs |
脚本报 ReferenceError |
用了 console / setTimeout / process 等沙盒不注入的对象 |
| 误以为脚本卡住 | 用了 async/await,沙盒不支持异步 |
| 以为"全部已读"=故障恢复 | 那只是阅读状态,与告警是否 clear、通知是否送达是三件事 |
一套能用的告警系统,技术含量不在脚本本身,而在这三个判断:什么是真的异常、谁必须知道、什么时候闭嘴。ThinkLink 把这三个判断都交给了脚本和属性配置——好处是灵活(同一套模型可以服务 500 台阈值不同的设备),代价是需要有人在上线前把规则想清楚。
本文的六招降噪里,最容易被忽略的是第 2 条(保持 desc 稳定)和第 3 条(clear 默认化)。这两条不写,系统上线第一周可能一切正常,第二周开始就没人愿意看告警列表了。
下一期预告(第 23 期):数据在本地不出厂区——ThinkLink Edge(TKE)边缘部署实战,从硬件选型、Docker Compose 起步到与云端物模型的双向迁移。