把工地遮挡和掉线接进项目部:setMessageCallback 订 alarm 与 deviceStatus

前言

以前工地安监的做法很朴素:镜头挂上,安全员盯 App 红点,出事了再翻回放、截图丢进项目部群。点位少、人盯得住的时候,这套还能交差。

点位一多就废了。大门被安全网蒙一下午,料场 4G 半夜断过电,App 里红点都在,项目部群是静的------第二天早会才知道。收藏回放解决不了「当时该谁去」。

开放平台这边,遮挡会出 videoBlind,掉线会出 offline,夜间料场走动会出 videoMotion。缺的不是再挂一路相机,而是用 setMessageCallbackalarmdeviceStatus 推到你自己的 HTTPS,再按点位进项目部。

读完按这个顺序交差:

  1. 在开放平台创建应用,把大门、料场设备绑进开发者资产。
  2. 签名自测通过,能拿到 accessToken
  3. callbackFlag 写成 alarm,deviceStatusgetMessageCallback 能读回同一条地址。
  4. 蒙镜头,桥接日志出现 videoBlind
  5. 拔网或断 4G,日志出现 offline,且 id-1
  6. 夜间料场走动进对应标段群;白天大门动检不进群。

工地两类漏报:镜头被挡(videoBlind)、4G 掉线(offline)。App 有红点,项目部没有人。本文用现行 OpenAPI 的 setMessageCallback 同时订 alarm + deviceStatus,带可运行 Node.js,验到群里响一条为止。


这篇会用到什么

类型见事件消息类型定义,流程见平台主动推送

  1. 同时订两类消息:alarmvideoBlind / videoMotiondeviceStatusoffline / online。只订一类,另一类你收不到。
  2. 回调必须先回 HTTP 200。多次无响应,平台停推,工地会在你不知道的时候变聋。
  3. 一个开发者账号一条 callbackUrl,十个标段按 did 找项目部,不要做十套服务。
  4. 遮挡、离线当 P0,马上叫人;动检只在夜间料场、仓库门进群。
  5. 上下线体的 id 固定是 -1cid-1 表示设备级,不能拿它去查告警详情。
  6. 4G 列表里的 sleep 是省电,不是掉线,不要按 offline 打电话。

你负责绑设备和项目部群,开放平台负责把事件推到你的 HTTPS。

text 复制代码
枪机 / 4G 云台
   ├─ 遮挡、动检  →  alarm        → videoBlind / videoMotion
   └─ 上下线      →  deviceStatus → offline / online
                         │
                         ▼
              你的 HTTPS(立刻 200)
                         │
                         ▼
              按 deviceId 找到标段 → 项目部群

为什么常见做法在工地扛不住

轮询 listDeviceDetailsByPage 适合对台账、对在线,不适合当值守主路径。扫得再勤也补不上推送那几秒,还烧调用次数。

只靠官方 App,红点停在安全员私人手机里,进不了标段群。卡没费、配电箱跳闸、镜头被防尘布挡住,都是当天的事,项目部要的是当时有人去,不是第二天的回放会。

一上来做原生客户端更没必要。项目部已经有钉钉或企微,缺的是桥,不是第三个 App。


动手:从创建应用到项目部群响了

部署只是开胃菜。真正过关的是:蒙镜头、断 4G、夜里在料场走一圈,群里各进一条该进的,不该进的别进。

第一步:创建应用,填齐现场台账

打开 乐橙开放平台 注册并创建应用。控制台里能看到 appId / appSecret 再往下做,没有这两样,后面签名都会废。把塔吊、大门、料场的设备绑进这个开发者资产,不要停在私人 App 号下。准备一个外网能 POST 进来的 HTTPS(联调用穿透,生产用正式证书)。

text 复制代码
IMOU_APP_ID=lcdxxxxxxxxx
IMOU_APP_SECRET=你的密钥
CALLBACK_URL=https://bridge.example.com/imou/callback
DEVICE_GATE=大门序列号
DEVICE_YARD=料场序列号
HOOK_GATE=大门项目部机器人地址
HOOK_YARD=料场项目部机器人地址
HOOK_DEFAULT=总群机器人地址

成功:控制台能看到应用,设备在资产里显示在线。

兜底:设备还在私人 App 下,先做绑定,再谈回调。

第二步:签名壳,先和文档案例对齐

请求走 https://openapi.lechange.cn/openapi/{method},壳子是 system + params + id。签名按开发规范time:{time},nonce:{nonce},appSecret:{appSecret} 做 MD5 小写 32 位。time 和服务器误差不能超过 5 分钟,nonce 5 分钟内不能重复,否则会撞 SN1005

javascript 复制代码
// openapi-client.js
const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');

const OPENAPI_BASE = 'https://openapi.lechange.cn/openapi';

function calcSign(time, nonce, appSecret) {
  const raw = `time:${time},nonce:${nonce},appSecret:${appSecret}`;
  return crypto.createHash('md5').update(raw, 'utf8').digest('hex');
}

async function callOpenApi(method, appId, appSecret, params = {}) {
  const time = Math.floor(Date.now() / 1000);
  const nonce = uuidv4();
  const body = {
    system: { ver: '1.0', appId, time, nonce, sign: calcSign(time, nonce, appSecret) },
    id: uuidv4(),
    params,
  };
  const res = await fetch(`${OPENAPI_BASE}/${method}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  const json = await res.json();
  if (!json.result || json.result.code !== '0') {
    const msg = json.result ? `${json.result.code} ${json.result.msg}` : JSON.stringify(json);
    throw new Error(`OpenAPI ${method} failed: ${msg}`);
  }
  return json.result.data;
}

module.exports = { callOpenApi, calcSign };

成功:calcSign(1706511734, 'f5a1ae2d-c09c-4d39-a744-83a5c2c653c2', 'test123456789test123456789') 得到 fd37b62889e4757c58b8f3bf05fb9976;再调 accessToken 能打印 token。

兜底:对不上标准案例,先别查业务接口,多半是字符串拼错。TK1002 再刷 token,不要每个请求都申。

第三步:动检要开,白天先别进群

工地白天吊臂、渣土车一直在动,动检如果全天进群,项目部会被刷死。PaaS 设备用 setDeviceCameraStatusmotionDetect(能力集是 MotionDetect,传参首字母小写),见设备能力开关。遮挡能不能出 videoBlind,以该机能力集为准,没有就不要硬开。

text 复制代码
token=上一步拿到的 accessToken
deviceId=料场序列号
channelId=0
enableType=motionDetect
enable=true

成功:接口 code=0;现场用布或安全网蒙几秒,后面第四、五步的日志里能看到 videoBlind

兜底:使能失败先查能力集,不要当成签名错了。大门白天的 videoMotion 只记日志,夜间料场才进群------规则写在第五步,这里先把开关打开。

第四步:登记回调,两类大类写在同一行

这篇必须跑通的接口是 setMessageCallbackstatus=oncallbackUrl 必填,必须公网可达。联调 basePush"2",以桥接日志为准。

text 复制代码
token=管理员 accessToken
status=on
callbackUrl=https://bridge.example.com/imou/callback
callbackFlag=alarm,deviceStatus
basePush=2
javascript 复制代码
// set-callback.js
require('dotenv').config();
const { callOpenApi } = require('./openapi-client');

(async () => {
  const token = (
    await callOpenApi('accessToken', process.env.IMOU_APP_ID, process.env.IMOU_APP_SECRET, {})
  ).accessToken;

  await callOpenApi('setMessageCallback', process.env.IMOU_APP_ID, process.env.IMOU_APP_SECRET, {
    token,
    status: 'on',
    callbackUrl: process.env.CALLBACK_URL,
    callbackFlag: 'alarm,deviceStatus',
    basePush: '2',
  });

  const current = await callOpenApi(
    'getMessageCallback',
    process.env.IMOU_APP_ID,
    process.env.IMOU_APP_SECRET,
    { token }
  );
  console.log(current);
})();

成功:getMessageCallback 读回的地址和 callbackFlag 与上面填写项一致。

兜底:只写出 alarm,掉线你永远收不到,改成 alarm,deviceStatus 再设一次。localhost 登记成功也收不到,先让公网 curl -X POST 打到自己的路径。

第五步:接收端先回 200,再按点位进群

同步路径零阻塞。多次不回 200,平台停推。上下线不是普通告警体,格式见事件消息格式定义

json 复制代码
{
  "id": -1,
  "did": "TESTDEVICEID001",
  "dname": "料场东侧4G",
  "cid": -1,
  "msgType": "offline",
  "type": 12,
  "time": 1787208164
}
javascript 复制代码
// bridge-server.js
require('dotenv').config();
const express = require('express');

const SITES = {
  [process.env.DEVICE_GATE]: { site: '1号门', hook: process.env.HOOK_GATE },
  [process.env.DEVICE_YARD]: { site: '料场', hook: process.env.HOOK_YARD },
};

const AFTER_HOURS = { start: 19, end: 7 };

function siteOf(did) {
  return SITES[did] || { site: did, hook: process.env.HOOK_DEFAULT };
}

function isAfterHours(ts) {
  const h = new Date(ts * 1000).getHours();
  return h >= AFTER_HOURS.start || h < AFTER_HOURS.end;
}

function priority(msgType) {
  if (msgType === 'videoBlind' || msgType === 'offline') return 'P0';
  if (msgType === 'videoMotion') return 'P2';
  return null;
}

function dedupeKey(ev) {
  const did = ev.did || ev.deviceId;
  if (ev.msgType === 'offline' || ev.msgType === 'online' || ev.id === -1) {
    return `${did}:${ev.cid}:${ev.msgType}:${ev.time}`;
  }
  return String(ev.id);
}

const seen = new Set();

async function toProjectDept(ev) {
  const pri = priority(ev.msgType);
  if (!pri) return;
  if (ev.msgType === 'videoMotion' && !isAfterHours(ev.time)) return;

  const key = dedupeKey(ev);
  if (seen.has(key)) return;
  seen.add(key);

  const { site, hook } = siteOf(ev.did || ev.deviceId);
  const when = new Date(ev.time * 1000).toLocaleString('zh-CN', { hour12: false });
  const text = `【工地 ${pri}】${site} ${ev.msgType}\n设备:${ev.did} 通道:${ev.cid}\n${when}\n去现场看,别只在群里回「收到」`;
  console.log('[项目部]', text, hook ? '→ hook' : '→ 仅日志');
}

const app = express();
app.use(express.json({ limit: '1mb' }));

app.post('/imou/callback', (req, res) => {
  res.status(200).json({ code: '0', msg: 'ok' });
  const body = req.body || {};
  const ev = body.msgType ? body : body.data || body;
  setImmediate(() => toProjectDept(ev).catch((e) => console.error(e)));
});

app.get('/health', (_req, res) => res.status(200).send('ok'));
app.listen(process.env.PORT || 8080);

成功:浏览器打开 /health 返回 okcurl -X POST 自己的回调,进程不堵、立刻 200。

兜底:把短信、钉钉、查库都写进同步路径,过不了这一步。先 200,再 setImmediate

第六步:现场验收,群响了才算闭环

按勾,不要跳步。

  1. 蒙大门或料场镜头,日志出现 videoBlind,对应标段群进一条 P0。
  2. 拔网或断 4G,日志出现 offlineid-1,群再进一条 P0。
  3. 晚上 19:00 后在料场走一圈,videoMotion 进料场群。
  4. 白天在大门前走动,不应进群,只留日志。

成功:上面四条都对得上。这就是「项目部群响了」。

兜底:有动检没掉线,回到第四步看 callbackFlag;有掉线蒙镜头没反应,查能力集,不要假设每台 4G 都能出 videoBlind

videoBlindoffline 不要和动检共用冷却。镜头被挡、设备没了,就是该马上有人去。online 只用来合掉线单,不要每上一次线再喊一嗓子。


群响了之后,再谈取证

最小闭环已经成立,才加第二层。仍用同一套现行 OpenAPI,不要另起一套协议。

推送体里的云录像 token 不是每台都有(文档写明常见于 platForm 为 4 的设备)。没有 token 时,用云录像或本地卡回放接口事后取证。叫醒走回调,取证走存储,不要塞进同一次同步请求。

4G 短抖动可以给 offline 加 2~3 分钟确认窗,避免插拔卡刷屏;确认窗不要套到遮挡上。生产把内存 seen 换成 Redis。派单字段、SLA、值班表是下一篇的事,这篇停在「群里有该去的人看到」。


联调会踩的坑

现象 多半是什么 先做什么
登记成功但一条都没有 URL 不是公网 / 证书 / 路径 404 curl -X POST 打自己的回调
收过一阵突然没了 同步路径堵了,多次没回 200,平台停推 修好后重新 setMessageCallback
有动检、没掉线 callbackFlag 只写了 alarm 改成 alarm,deviceStatus 再设一次
有掉线、蒙镜头没反应 机型没有遮挡能力 查能力集,用布遮实测
白天群被吊车刷爆 动检全天进群 动检加夜间窗;遮挡/离线不要加这段窗
sleep 当成掉线打电话 4G 省电 列表字段和回调 offline 分开看
拿上下线 id 去查告警 文档写明固定为 -1 did + cid + time 做幂等键

结尾

这篇省下的不是多一路镜头,而是安全员不用第二天翻回放,才在项目部群里问「谁去看过」。

只做值班大屏预览、不接群的,去看轻应用多路预览那篇就够了,不必在这里把回调搭起来。偶尔装两台相机、出事了有人盯 App 的散户,也没必要上这一套。

appSecret、机器人地址只放环境变量。公网回调建议加自有校验,日志里的手机号打码。接口以现行文档为准,不要混已标注不再维护的旧协议栏目。

相关推荐
老孙讲技术2 小时前
关店后有人进门,监控值班群却没响:用 setMessageCallback 订 human,再用 aiHuman 打开人形
后端·物联网·音视频开发
LEE2 小时前
原来 Claude Code 最厉害的工具,一行代码都不写
前端·后端
老孙讲技术2 小时前
把园区几百路摄像头收进值班台账:bindDevice 入账与 listDeviceDetailsByPage 翻页
后端·物联网·音视频开发
用户7813667114452 小时前
emplace_back vs push_back 详解
后端
老孙讲技术2 小时前
把没网没电的户外点位接进出画值班台:unBindDeviceInfo 核 SIMCard 与 bindDeviceLive
后端·物联网·音视频开发
万物智能2 小时前
硬件调试三板斧—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
后端·架构
小夏coding3 小时前
从"一把梭"到"精妙拆解" —— 滑动窗口计时框架的设计演进
java·后端
长大19883 小时前
Node.js 22+ 特性实战:用内置能力写微服务与 CLI,能少装一堆依赖
后端
苍何3 小时前
飞书,成了豆包工作的“超级上下文”
后端