把园区几百路摄像头收进值班台账:bindDevice 入账与 listDeviceDetailsByPage 翻页

目录


东门 NVR、仓库 4G、周界枪机,为什么对不上账

以前园区安防的做法很朴素:东门一套 NVR,仓库几台 4G 机挂在保安私人 App,周界枪机再进另一套厂商软件。值班室三块屏,早会用 Excel 对数。点位少、人盯得住的时候,这套还能交差。

点位涨到几百路就废了。东门通道在 NVR 里是 12,仓库机还在私人号下,周界掉线要第二天对表才知道。再买十台相机,账还是三本。

缺的不是再挂一路镜头,而是用现行 OpenAPI 把设备 bindDevice 进同一个开发者资产,再用 listDeviceDetailsByPage 按页拉齐,摊进你自己的园区值班台账。

读完按这个顺序交差:

  1. 在开放平台创建应用,拿到 appId / appSecret
  2. 签名自测通过,能拿到 accessToken
  3. 抽几台核绑定,再 bindDevicecheckDeviceBindOrNot 读回 isMine=true
  4. pageSize=50 翻页,扫完账号下全部设备。
  5. 值班台账按通道计路,东门 / 仓库 / 周界能数清。
  6. 抽一台拔网,台账里对应点位变成 offline,在线数对得上。

工业园区几百路分散在 NVR、4G、私人 App。本文用现行 OpenAPI 的 bindDevice 统一入账、listDeviceDetailsByPage 翻页对账,带可运行 Node.js,验到本地 /inventory 数清路数为止。


这篇会用到这几项能力

查询见设备查询说明,添加见设备添加说明

  1. bindDevice:把设备绑到开发者账号。code 按机型传底部安全码或改过的设备密码;没安全码且未改密,文档允许传空。
  2. checkDeviceBindOrNot:先看 isBind / isMine。别人账上的机,绑不进来。
  3. listDeviceDetailsByPage:分页拉设备详情。pageSize 范围 1~50,page 从 1 起。几百路就是翻几页,不是一次拉全量。
  4. 返回里的 deviceStatus / channelStatusonlineofflinesleepupgradingsleep 是休眠,不要按掉线叫人。
  5. catalog 能区分 NVRIPC。几百路不等于几百台:一台 NVR 下面有多条 channelList
  6. accessToken 有效期 3 天,报 TK1002 再刷,不要每个请求都申。

你负责把点位绑进开发者资产、写清东门仓库周界,开放平台负责按页吐设备详情和在离线。

text 复制代码
枪机 / 4G / NVR 通道
        │
        ▼
  bindDevice 入同一开发者账号
        │
        ▼
  listDeviceDetailsByPage(每页最多 50)
        │
        ▼
  你的值班台账(按 channelId 计路)
        │
        ▼
  东门 / 仓库 / 周界 在线数

轮询、官方 App、一上来写原生,园区为什么扛不住

轮询适合对台账、对在线,前提是设备已经在同一个开发者账号下。三套厂商各轮各的,不是统一接入,只是三份定时任务。

只靠官方 App,红点停在保安私人手机里。交接班对不上序列号,仓库机跟着人走,园区后台永远是「少几路」。

一上来做原生客户端更没必要。园区已经有 OA 和值班大屏,缺的是 HTTP 把资产拉进自己的库,不是第三个看监控的 App。


动手:从创建应用到值班台账数清

部署只是开胃菜。真正过关的是:账号下的机全部出现在 /inventory,路数对、区域对,抽一台掉线数字会变。

第一步:创建应用,先填园区点位表

打开 乐橙开放平台 注册并创建应用。控制台「我的应用 - 应用信息」能看到 appId / appSecret 再往下做。没有这两样,后面签名都会废。

先不要写接口。用一张表把现场点位收拢:序列号、区域、通道、验证码来源。NVR 只记一台设备,通道另行列。设备先配网上线,能在官方 App 预览,再谈绑定。

text 复制代码
IMOU_APP_ID=lcdxxxxxxxxx
IMOU_APP_SECRET=你的密钥
PORT=8080

# 园区点位(deviceId 或 deviceId:channelId)
ZONE_EAST_GATE=东门NVR序列号
ZONE_WAREHOUSE=仓库4G序列号
ZONE_PERIMETER=周界枪机序列号

成功:控制台能看到应用;点位表每行都有序列号和区域,没有「在某某私人 App 里」。

兜底:还停在私人号下的机,先从点位表标出来,第三步再核绑定。控制台都没有应用,不要往下抄代码。

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

请求走 https://openapi.lechange.cn/openapi/{method},壳子是 system + params + id。签名按开发规范time:{time},nonce:{nonce},appSecret:{appSecret} 做 MD5 小写 32 位。time 和服务器误差不能超过 5 分钟,否则 SN1002nonce 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 和 expireTime

兜底:对不上标准案例,先别查业务接口,多半是字符串拼错。TK1002 再刷 token。签名算法若控制台提示不一致,以现行开发规范为准,不要混网上过时示例。

第三步:先核绑定,再一台一台 bindDevice

这篇入账接口是 bindDevice。先 checkDeviceBindOrNotisBind=false 再绑;isMine=true 已经是你的;isBind=trueisMine=false 在别人账上,先解绑或走交接,不要死磕。

text 复制代码
token=上一步拿到的 accessToken
deviceId=仓库4G序列号
code=底部8位安全码或改过的设备密码

未改密且标签没有 8 位安全码,按文档把 code 传空。一批机不要写循环闷头绑,先抽东门、仓库、周界各一台。

应用开发写明:部分新出厂或升级后的设备,可能无法只靠 HTTP bindDevice 完成绑定,要结合客户端 SDK。联调绑失败、核绑定又显示未在本账号,先走这条说明,不要改签名。

成功:bindDevice 返回 code=0;再查一次,isBind=trueisMine=true

兜底:已经是别人的资产,先让现场从原账号解绑。新机 HTTP 绑不上,按应用开发说明改 SDK 路径。

第四步:pageSize 写成 50,把账号翻完

这篇对账接口是 listDeviceDetailsByPagepageSize 最大 50,自有资产用 source=bind;连分享进来的也要入账,用默认的 bindAndShare。翻页直到某一页 deviceList 为空,或累计条数不再增加。

javascript 复制代码
// sync-devices.js
require('dotenv').config();
const { callOpenApi } = require('./openapi-client');

async function listAllDevices(appId, appSecret, token) {
  const all = [];
  let page = 1;
  const pageSize = 50;
  while (true) {
    const data = await callOpenApi('listDeviceDetailsByPage', appId, appSecret, {
      token,
      page,
      pageSize,
      source: 'bind',
    });
    const chunk = data.deviceList || [];
    all.push(...chunk);
    if (chunk.length < pageSize) break;
    page += 1;
  }
  return all;
}

function flattenChannels(deviceList, zoneOf) {
  const rows = [];
  for (const d of deviceList) {
    const channels = d.channelList && d.channelList.length ? d.channelList : [{ channelId: 0, channelStatus: d.deviceStatus }];
    for (const ch of channels) {
      rows.push({
        deviceId: d.deviceId,
        deviceName: d.deviceName,
        catalog: d.catalog,
        deviceStatus: d.deviceStatus,
        channelId: ch.channelId,
        channelStatus: ch.channelStatus || d.deviceStatus,
        deviceVersion: d.deviceVersion,
        zone: zoneOf(d.deviceId),
      });
    }
  }
  return rows;
}

module.exports = { listAllDevices, flattenChannels };

成功:控制台打印出 deviceIdcatalogdeviceStatusdeviceVersion;有 NVR 时,同一 deviceId 下出现多条 channelId。400 路大约 8 页,几百路听起来吓人,其实就是 pageSize=50 翻几页。把每次返回的 count 和本页 deviceList.length 记下来,方便对「少了一页」这种问题。

兜底:只打了第 1 页就停,台账会永远少一大截。pageSize 写成 100,以现行文档为准,合法值是 1~50。分享进来的机要用 bindAndShare,只写 bind 会漏。

第五步:值班台账服务,按通道计路

本地映射区域,摊平通道,露出 /inventory。这是接收侧:开放平台吐列表,你的服务收成园区能看的一张账。还没数清之前,不要去创建直播。

text 复制代码
# zones.json 填写模板(对齐写字段)
{
  "东门NVR序列号": "东门",
  "仓库4G序列号": "仓库",
  "周界枪机序列号": "周界"
}
javascript 复制代码
// inventory-server.js
require('dotenv').config();
const express = require('express');
const { callOpenApi } = require('./openapi-client');
const { listAllDevices, flattenChannels } = require('./sync-devices');
const ZONES = require('./zones.json');

function zoneOf(deviceId) {
  return ZONES[deviceId] || '未分区';
}

let cache = { rows: [], syncedAt: 0 };

async function refresh() {
  const token = (
    await callOpenApi('accessToken', process.env.IMOU_APP_ID, process.env.IMOU_APP_SECRET, {})
  ).accessToken;
  const devices = await listAllDevices(process.env.IMOU_APP_ID, process.env.IMOU_APP_SECRET, token);
  cache = { rows: flattenChannels(devices, zoneOf), syncedAt: Date.now() };
}

const app = express();
app.get('/health', (_req, res) => res.status(200).send('ok'));
app.get('/inventory', async (_req, res) => {
  if (!cache.rows.length) await refresh();
  const online = cache.rows.filter((r) => r.channelStatus === 'online').length;
  const offline = cache.rows.filter((r) => r.channelStatus === 'offline').length;
  const sleep = cache.rows.filter((r) => r.channelStatus === 'sleep').length;
  res.json({
    totalChannels: cache.rows.length,
    online,
    offline,
    sleep,
    syncedAt: cache.syncedAt,
    rows: cache.rows,
  });
});
app.post('/inventory/refresh', async (_req, res) => {
  await refresh();
  res.json({ totalChannels: cache.rows.length, syncedAt: cache.syncedAt });
});
app.listen(process.env.PORT || 8080);

成功:浏览器打开 /health 返回 okGET /inventory 能看到 totalChannels、按区域分好的 rows,以及 deviceVersion。值班大屏只读这个 JSON,不要让前端直接拿 appSecret 去翻页。

兜底:数字对不上现场,先看是不是按设备计台、没按通道计路。未分区 太多,回去补 zones.json,不要先改签名。refresh 里已经申过 token,同一次同步不要再打 accessToken

第六步:抽一台拔网,台账变了才算闭环

按勾,不要跳步。

  1. /inventorytotalChannels 对得上点位表(NVR 按通道加总)。
  2. 东门、仓库、周界三行都能对上 deviceId
  3. 抽仓库或周界一台拔网 / 断 4G,调 POST /inventory/refresh,该行 channelStatusdeviceStatus 变为 offlineoffline 计数加一。
  4. 插回网线,再刷一次,回到 online

成功:上面四条都对得上。这就是「值班台账数清了」。

兜底:列表有机、刷新状态不变,隔一两分钟再刷;4G 上的 sleep 不要按掉线打电话。单台复核可用 listDeviceDetailsByIdsdeviceOnline。注意:deviceOnlineonLine0/1/3/4,和列表里的英文枚举不是同一套字,对账时不要混着比。


台账对上之后,再开远程看和回放

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

值班员要点开东门看一眼:先 bindDeviceLive 创建设备源直播,再 getLiveStreamInfo 取 HLS。没创建过直播,后一个接口拿不到地址。直播地址对外等于公开画面,只给值班内网,不要进公共群。

事后取证:同一天时段用 queryCloudRecords 查云录像片段(queryRange 最大 100),再用 createDeviceRecordHls 生成回放地址。beginTime / endTime 格式是 yyyy-MM-dd HH:mm:ss,文档写明不支持跨天。recordTypecloudRecordlocalRecord,以该点位是否开通云存储、是否有本地卡为准。

告警进工单、遮挡掉线进群,是下一篇的事。这篇停在「一张账能数清,抽一台掉线数字会变」。


联调会踩的坑

现象 多半是什么 先做什么
台账只有几十路 只拉了第 1 页 pageSize=50 一直翻到不足一页
现场 200 路,台账 40 台 按设备计台,没按通道计路 展开 channelList,NVR 按 channelId
仓库机列表里没有 还在私人 App,isMine=false checkDeviceBindOrNot,再 bindDevice
HTTP 绑失败 新机 / 新固件不走纯 HTTP 绑定 应用开发的 SDK 说明
sleep 当成掉线打电话 4G 省电 offline 分开看
签名对不上标准案例 原始串拼错或混了过时算法 先对齐 MD5 案例,再谈业务
隔天一批接口全挂 token 过了约 3 天 收到 TK1002 再刷,不要每个请求都申
列表英文状态和单查对不上 deviceOnline 用的是 0/1/3/4 两套枚举分开映射

真正省下的不是再买一台相机

这篇省下的不是多一路镜头,而是值班室不用三块屏、三份 Excel,才能回答「园区现在在线多少路、掉的是东门还是仓库」。

只做网页预览、不建资产台账的,去看轻应用嵌入那条线就够了,不必在这里把翻页和区域映射搭起来。家里两台相机、出事了有人盯 App 的散户,也没必要上这一套。

appSecret、设备密码只放环境变量。直播地址当密钥管。日志里的序列号对外部打码。接口以现行文档为准,不要混已标注不再维护的栏目。

相关推荐
用户7813667114452 小时前
emplace_back vs push_back 详解
后端
老孙讲技术2 小时前
把没网没电的户外点位接进出画值班台:unBindDeviceInfo 核 SIMCard 与 bindDeviceLive
后端·物联网·音视频开发
万物智能2 小时前
硬件调试三板斧—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
后端·架构
小夏coding3 小时前
从"一把梭"到"精妙拆解" —— 滑动窗口计时框架的设计演进
java·后端
长大19883 小时前
Node.js 22+ 特性实战:用内置能力写微服务与 CLI,能少装一堆依赖
后端
苍何3 小时前
飞书,成了豆包工作的“超级上下文”
后端
苍何3 小时前
会用 AI 做图,已经不够了(分享 3 个牛逼设计 skill)
后端
明月_清风3 小时前
发现一个超系统的 AI Agent 学习地图 —— Agent Atlas 推荐
人工智能·后端·agent
大勇前进4 小时前
设计模式在 JS 中的应用:用 TS 重写经典模式,结合 React / Vue 真实业务场景
后端