轮询查告警太慢、推送总丢消息?一文打通乐橙设备告警与自有业务系统的消息联动
系列:乐橙接入实战 · 消息联动篇
封面:imou-alarm-mqtt-csdn-cover-orange.png
文档:乐橙云开发文档 · 注册:乐橙开放平台
协议说明:本文仅使用当前维护的 OpenAPI(域名
openapi.lechange.cn,请求体含system+params,见开发规范)。文档中的「旧版本协议」栏目已声明后续不再维护,请勿在新项目中引用该模块下的接口;下文代码与链接均来自现行文档。
凌晨两点十七分,值班手机终于响了——不是告警,是老板在群里 @ 所有人:「仓库摄像头明明触发了动检,你们后台怎么还是绿的?」我们翻了日志:设备在线、云录像正常,问题出在消息链路——业务系统每 30 秒轮询一次 getAlarmMessage,动检发生在两次轮询之间,告警被「吞」了;更糟的是,上次回调服务超时没回 200,乐橙云已经悄悄停推了。那一夜我们意识到:设备在线 ≠ 告警可达。
为什么这件事值得现在做?
乐橙开放平台日均消息推送超过 10 亿条(官网数据),覆盖动检、人形、AI 智见、IoT 物模型、客流统计等上百种事件类型。对开发者而言,消息能力直接决定:
- 报警运营平台能否秒级触达值守人员
- 零售/社区方案能否把「视频事件」变成可编排的业务动作
- 智能家居/养老场景能否做设备联动(门锁、传感器、大屏)
先厘清一个常见误解
很多同学习惯用 MQTT 对接 IoT 设备,会下意识问:「乐橙有没有 MQTT Broker?」
查阅现行文档后可以明确:乐橙开放平台的消息推送机制是 HTTP 回调(Webhook),而非让开发者直连平台 MQTT。标准路径是:
- 调用
setMessageCallback配置公网可访问的回调 URL - 设备产生事件后,平台 主动 POST 到你的服务
- 你的服务标准化消息后,再分发到 MQTT / WebSocket / 消息队列 等内部总线
这不是妥协,而是合理的分层:乐橙负责设备侧采集与云端投递,你负责业务侧路由与联动。
技术背景与解决思路
现行 OpenAPI 统一走 https://openapi.lechange.cn/openapi/{method},请求体包含 system(含签名)和 params(业务参数),规范见开发规范。
消息联动推荐架构:
双通道设计更稳:
- 推送通道(主):
setMessageCallback实时接收 - 补偿通道(辅):
getAlarmMessage按时间段补查漏网告警
3.1 环境准备
# 项目结构
imou-alarm-bridge/
├── .env
├── package.json
├── src/
│ ├── imou-client.js # OpenAPI 签名与调用
│ ├── callback-server.js # 接收乐橙推送
│ └── mqtt-bridge.js # 转发到 MQTT
└── docker-compose.yml # 本地 MQTT Broker
.env(切勿提交到 Git):
IMOU_APP_ID=lcdxxxxxxxxx
IMOU_APP_SECRET=your_app_secret_here
IMOU_CALLBACK_URL=https://your-domain.com/imou/callback
MQTT_BROKER_URL=mqtt://localhost:1883
MQTT_TOPIC_PREFIX=imou/alarm
踩坑 1:
appId/appSecret在 乐橙开放平台 → 开发中心 → 我的应用 → 应用信息 中获取,只放服务端环境变量。
3.2 OpenAPI 客户端(签名 + Token 缓存)
// src/imou-client.js
import crypto from 'crypto';
import { randomUUID } from 'crypto';
const API_BASE = 'https://openapi.lechange.cn/openapi';
export function calcSign(time, nonce, appSecret) {
const raw = `time:${time},nonce:${nonce},appSecret:${appSecret}`;
return crypto.createHash('md5').update(raw, 'utf8').digest('hex');
}
function buildBody(appId, appSecret, params = {}) {
const time = Math.floor(Date.now() / 1000);
const nonce = randomUUID().replace(/-/g, '').slice(0, 32);
return {
system: {
ver: '1.0',
appId,
time,
nonce,
sign: calcSign(time, nonce, appSecret),
},
id: randomUUID(),
params,
};
}
export async function callOpenApi(method, appId, appSecret, params = {}) {
const res = await fetch(`${API_BASE}/${method}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(buildBody(appId, appSecret, params)),
});
const json = await res.json();
if (json.result?.code !== '0') {
throw new Error(`[${method}] ${json.result?.code}: ${json.result?.msg}`);
}
return json.result.data;
}
let cachedToken = null;
let tokenExpireAt = 0;
export async function getAdminToken(appId, appSecret) {
if (cachedToken && Date.now() < tokenExpireAt - 60_000) {
return cachedToken;
}
const data = await callOpenApi('accessToken', appId, appSecret, {});
cachedToken = data.accessToken;
tokenExpireAt = Date.now() + data.expireTime * 1000;
return cachedToken;
}
签名自测(与官方标准案例对齐,算不对后面全部接口都会 SN1001):
const sign = calcSign(
1706511734,
'f5a1ae2d-c09c-4d39-a744-83a5c2c653c2',
'test123456789test123456789'
);
console.log(sign); // fd37b62889e4757c58b8f3bf05fb9976
3.3 注册消息回调(核心一步)
// scripts/setup-callback.js
import 'dotenv/config';
import { getAdminToken, callOpenApi } from '../src/imou-client.js';
const { IMOU_APP_ID, IMOU_APP_SECRET, IMOU_CALLBACK_URL } = process.env;
const token = await getAdminToken(IMOU_APP_ID, IMOU_APP_SECRET);
await callOpenApi('setMessageCallback', IMOU_APP_ID, IMOU_APP_SECRET, {
token,
status: 'on',
callbackUrl: IMOU_CALLBACK_URL,
callbackFlag: 'alarm,deviceStatus,iot,numberstat,faceAnalysis',
basePush: '2',
});
console.log('消息回调注册成功');
callbackFlag 与事件大类对应关系(事件消息类型定义):
| callbackFlag | 覆盖能力 |
|---|---|
alarm | 动检 videoMotion、人形 human、AI 入侵等上百种告警 |
deviceStatus | 设备/通道 online / offline |
iot | IoT 物模型 iotEvent |
numberstat | 客流 numberstat、滞留 manyStay |
faceAnalysis | 人脸检测/比对等智能消息 |
踩坑 2:
callbackUrl必须公网 HTTPS 可访问。本地开发用 ngrok / frp 穿透,内网地址平台推不过来。
踩坑 3:status为on时callbackUrl和callbackFlag必填,漏填会静默失败或返回参数错误。
3.4 回调接收服务(必须快速返回 200)
平台要求:回调服务务必返回 HTTP 200,否则多次不响应将停止推送(事件消息推送流程)。
// src/callback-server.js
import express from 'express';
import { publishToMqtt } from './mqtt-bridge.js';
const app = express();
app.use(express.json({ limit: '2mb' }));
app.post('/imou/callback', async (req, res) => {
res.status(200).send('OK');
const payload = req.body;
try {
await handleImouMessage(payload);
} catch (err) {
console.error('[callback] async handle error:', err);
}
});
async function handleImouMessage(msg) {
const normalized = normalizeMessage(msg);
if (!normalized) return;
const topic = `${process.env.MQTT_TOPIC_PREFIX}/${normalized.deviceId}/${normalized.msgType}`;
await publishToMqtt(topic, normalized);
console.log(`[alarm] ${normalized.msgType} from ${normalized.deviceId}`);
}
function normalizeMessage(raw) {
if (raw.did && raw.msgType) {
return {
source: 'imou',
msgType: raw.msgType,
alarmId: raw.id,
deviceId: raw.did,
channelId: raw.cid,
channelName: raw.cname,
timestamp: raw.time,
token: raw.token ?? null,
desc: raw.desc ?? null,
raw,
};
}
if (raw.msgType === 'iotEvent') {
return {
source: 'imou-iot',
msgType: raw.content?.event,
alarmId: raw.alarmId,
deviceId: raw.did,
productId: raw.pid,
timestamp: raw.time,
content: raw.content,
raw,
};
}
if (raw.msgType === 'numberstat') {
return { source: 'imou-flow', ...raw };
}
return { source: 'imou-unknown', raw };
}
app.listen(3000, () => console.log('callback server :3000'));
动检告警原始报文示例(事件消息格式定义):
{
"id": 2447736561,
"appId": "lcdxxxxxxxxx",
"did": "TESTQWERXXXX",
"cid": 0,
"msgType": "videoMotion",
"time": 1475052555,
"cname": "仓库东门",
"remark": "",
"token": "f2dc8c09eeae4b5bad6abf522c93d825"
}
3.5 MQTT 桥接层(业务系统真正订阅的地方)
// src/mqtt-bridge.js
import mqtt from 'mqtt';
let client;
export function getMqttClient() {
if (!client) {
client = mqtt.connect(process.env.MQTT_BROKER_URL, {
clientId: `imou-bridge-${Date.now()}`,
clean: true,
});
}
return client;
}
export function publishToMqtt(topic, payload) {
return new Promise((resolve, reject) => {
getMqttClient().publish(
topic,
JSON.stringify(payload),
{ qos: 1 },
(err) => (err ? reject(err) : resolve())
);
});
}
业务侧订阅示例(Python,接入运营大屏):
import json
import paho.mqtt.client as mqtt
def on_message(client, userdata, msg):
alarm = json.loads(msg.payload)
print(f"[{alarm['msgType']}] 设备 {alarm['deviceId']} 通道 {alarm['channelId']}")
# 联动:弹窗、工单、电话提醒、写数据库...
client = mqtt.Client()
client.on_message = on_message
client.connect("your-mqtt-broker", 1883)
client.subscribe("imou/alarm/#", qos=1)
client.loop_forever()
本地 MQTT Broker(docker-compose.yml):
services:
mosquitto:
image: eclipse-mosquitto:2
ports:
- "1883:1883"
volumes:
- ./mosquitto.conf:/mosquitto/config/mosquitto.conf
3.6 补偿查询:漏推时的兜底
// scripts/backfill-alarms.js
import 'dotenv/config';
import { getAdminToken, callOpenApi } from '../src/imou-client.js';
const token = await getAdminToken(process.env.IMOU_APP_ID, process.env.IMOU_APP_SECRET);
const data = await callOpenApi('getAlarmMessage', process.env.IMOU_APP_ID, process.env.IMOU_APP_SECRET, {
token,
deviceId: 'TESTQWERXXXX',
channelId: '0',
beginTime: '2025-06-22 00:00:00',
endTime: '2025-06-22 23:59:59',
count: 30,
nextAlarmId: '-1',
});
for (const alarm of data.alarms ?? []) {
console.log(alarm.alarmId, alarm.type, alarm.aiCopyWriting, alarm.aiTag);
}
若设备开通了智见云存储 / AI 智见套餐,返回中会附带 aiCopyWriting(大模型分析文案)、securityRiskLevel、aiTag(人/车/宠物/包裹),可直接用于告警分级,无需你再跑一遍视觉模型。
踩坑 4:AI 类推送的图片 URL 最长保存 1 天,收到消息后务必异步下载到自己的 OSS,否则第二天链接失效。
踩坑 5:设备必须先绑定到开发者账号资产池,listDeviceDetailsByPage能查到且deviceStatus=online,否则根本没有消息可推。
3.7 端到端验证清单
| 步骤 | 验证方式 | 预期 |
|---|---|---|
| 1 | calcSign 标准案例 | 输出 fd37b62889e4757c58b8f3bf05fb9976 |
| 2 | accessToken | code=0,拿到 accessToken |
| 3 | setMessageCallback | code=0,操作成功 |
| 4 | 触发设备动检 | 回调服务日志出现 videoMotion |
| 5 | MQTT 订阅端 | 收到 imou/alarm/{deviceId}/videoMotion |
| 6 | 回调故意延迟 10s 不回 200 | 平台将标记异常,切勿在生产尝试 |
边界讨论
| 场景 | 建议 |
|---|---|
| 只需要告警,不需要上下线 | callbackFlag 只填 alarm |
| 多租户 SaaS | 按 appId / 设备分组映射不同 MQTT topic 前缀 |
| 乐橙 APP 设备与开放平台设备混合 | 谨慎设置 basePush:1 推送 APP 设备消息,2 不推送 |
| 子账号权限隔离 | 子账户 token 调 getAlarmMessage 需 Permission: Alarm |
性能与可靠性
- 回调异步化:收到请求先
200,再入内存队列 / Redis Stream / Kafka,避免业务逻辑阻塞导致平台停推。 - 幂等去重:以
alarmId或id+time+did做去重键,防止网络重试导致重复工单。 - QoS 选择:MQTT 发布建议
qos=1;核心告警可同时写 DB 再 ack。 - Token 刷新策略:管理员 token 约 3 天有效,遇
TK1002刷新;不要每次请求都调accessToken。 - nonce 唯一性:5 分钟内不可重复,高并发时用 UUID 足够;批量脚本注意别复用同一 nonce。
生产环境注意事项
- 回调 URL 走 HTTPS,建议加固 IP 白名单或签名校验(平台推送体可结合业务侧二次验证)
- 监控三项指标:回调成功率、MQTT 发布延迟、补查差集数量(差集突增说明推送链路异常)
- 下线维护时调用
setMessageCallback设status: off,避免推送到已销毁的 endpoint
总结
乐橙开放平台的消息联动,本质上是 「云端 HTTP 推送 + 开发者侧消息路由」 的组合:
- 用
setMessageCallback把设备侧上百种事件实时推到你的公网服务 - 用
getAlarmMessage做补偿与历史查询 - 用 MQTT / 自有消息总线对接运营大屏、App、工单、电话提醒等业务终端
我们那次「仓库动检漏报」的教训,根源不是设备不行,而是把轮询当推送、把回调当可选项。换成「回调为主、MQTT 分发、补查兜底」之后,告警到达时间从分钟级降到了秒级。
延伸阅读
开始动手
如果你正在做报警运营、社区安防、零售巡店、智慧养老等视频场景,消息联动往往是用户体验的分水岭——视频能看只是起点,告警能及时触达才是价值闭环。
乐橙开放平台(open.imou.com) 以视频技术和安全为核心,开放低代码开发组件,一站式助力第三方厂商与个人开发者快速、低成本落地视频场景应用。新用户注册可领取 10 个设备接入额度 和 1Mbps 媒体带宽,足够完成本文整套回调 + MQTT 联调。
270

被折叠的 条评论
为什么被折叠?



