把没网没电的户外点位接进出画值班台:unBindDeviceInfo 核 SIMCard 与 bindDeviceLive

0 阅读11分钟

目录


鱼塘卡口和工地门口,拉线方案为什么废了

以前户外安防的做法很朴素:找电工拉 220V,再找弱电铺网线,镜头挂上,保安盯官方 App。门口有配电箱、机房就在五十米外时,这套还能交差。

点位一出围墙就废了。鱼塘对岸没电杆,工地大门临时围挡每周挪一次,林班卡口更没有网线可挖。再买十台网线枪机,现场还是「没网没电」。太阳能板加 4G 卡能让机器自己活着,但画面还停在安装师傅的私人 App 里。

缺的不是再买一台相机,而是用现行 OpenAPI:先 unBindDeviceInfo 核这台机是不是走 SIMCard、4G 是否已经 online,再 bindDevice 进开发者资产,用 bindDeviceLive 把 HLS 接到你自己的值班台。

读完按这个顺序交差:

  1. 在开放平台创建应用,拿到 appId / appSecret
  2. 签名自测通过,能拿到 accessToken
  3. unBindDeviceInfo 读回 wifiConfigModeSIMCard,且 statusonlinesleep
  4. checkDeviceBindOrNotbindDeviceisMine=true
  5. bindDeviceLive 出 HLS,本地 /preview 能播、封面图能打开。
  6. 抽一张卡或断供电,台账里对应点位变成 offline,不要把 sleep 当成掉线。

户外没市电、没网线。本文用现行 OpenAPI 的 unBindDeviceInfoSIMCard 上线,再 bindDeviceLive 把画面接到本地值班页,带可运行 Node.js,验到 /preview 出画为止。


需要用到的这几项能力

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

  1. unBindDeviceInfo:绑之前看平台支不支持、设备在不在、配网模式里有没有 SIMCardstatusonline / offline / sleep / upgrading
  2. checkDeviceBindOrNot + bindDevice:先看 isBind / isMine,再绑进开发者账号。code 按机型传底部安全码或改过的设备密码。
  3. listDeviceDetailsByIds / deviceOnline:绑完后复核在离线。列表用英文枚举;deviceOnlineonLine0/1/3/44 是休眠。
  4. bindDeviceLive + getLiveStreamInfo:创建设备源直播,再取 HLS。没创建过,后一个接口拿不到地址。
  5. deviceStatus 里的 sleep:4G 太阳能常见省电态,不是掉线。能力集里有 CloseDormant 才谈 closeDormant,以该机返回为准。
  6. accessToken 有效期 3 天,报 TK1002 再刷,不要每个请求都申。

你负责把太阳能板朝向、SIM 卡套餐和点位表绑进自己的值班页,开放平台负责核蜂窝是否上线、把 HLS 吐给你。

太阳能 + 电池 + 4G/5G SIM
        │
        ▼
  设备自己上蜂窝(wifiConfigMode 含 SIMCard)
        │
        ▼
  unBindDeviceInfo 核 exist / online|sleep
        │
        ▼
  bindDevice 入开发者账号
        │
        ▼
  bindDeviceLive → 你的 /preview 出画

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

轮询适合对台账、对在离线,前提是设备已经在同一个开发者账号下,并且你分得清 sleepoffline。机还在安装师傅私人号里,你轮的是空账。

只靠官方 App,红点停在包工头手机里。卡欠费、电池见底、天线被雨布捂住,值班室要第二天对表才知道。户外要的是自己的页能出画、掉线能进自己的系统,不是再下一个看监控的 App。

一上来写原生更没必要。项目部已经有 Web 值班台或钉钉,缺的是 HTTP 把 4G 点位收进来,不是第三个客户端。


动手:从创建应用到本地出画

部署只是开胃菜。真正过关的是:卡插上、板子朝南,本地 /preview 能播这路户外画面。画面没出来之前,不要去订回调、不要去取回放。

第一步:创建应用,先填户外点位表

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

先不要写接口。现场先让设备自己活着:太阳能板朝南、电池有电、4G/5G 卡有流量且已激活。官方 App 里能预览,再谈绑定。wifiTransferMode 里的 5Ghz 是 Wi-Fi 频段,不是 5G 蜂窝;蜂窝配网看 SIMCard。板子朝北、天线捂在雨布里,开放平台帮不了你把太阳搬过来。

IMOU_APP_ID=lcdxxxxxxxxx
IMOU_APP_SECRET=你的密钥
PORT=8080

# 户外点位(先一张表,再填进环境变量)
SITE_POND=鱼塘对岸序列号
SITE_GATE=工地门口序列号
DEVICE_CODE_POND=底部8位安全码或改过的设备密码
DEVICE_CODE_GATE=底部8位安全码或改过的设备密码
# sites.json 填写模板(对齐写字段)
{
  "鱼塘对岸序列号": {
    "name": "鱼塘对岸",
    "power": "solar",
    "network": "4G",
    "channelId": "0"
  },
  "工地门口序列号": {
    "name": "工地门口",
    "power": "solar",
    "network": "5G",
    "channelId": "0"
  }
}

成功:控制台能看到应用;点位表每行都有序列号、供电、卡类型,官方 App 已能预览。
兜底:App 里都是离线,先查卡费、电池电压、天线,不要先改签名。控制台都没有应用,不要往下抄代码。

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

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

// 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。签名算法若控制台提示不一致,以现行开发规范为准,不要混网上过时示例。

第三步:先 unBindDeviceInfo,再谈绑定

这篇核蜂窝的接口是 unBindDeviceInfo。绑之前打这一枪:support=truedeviceExist=existwifiConfigMode 里能看到 SIMCardstatus=offline 先别绑,机还没从蜂窝爬上来。

token=上一步拿到的 accessToken
deviceId=鱼塘对岸序列号

status=sleep 不是失败。太阳能 4G 为了省电会睡,和卡没费、电池空了的 offline 不是一回事。wifiConfigMode 是逗号分隔的配对模式列表,缺 SIMCard 就不要按 4G 方案往下做,以该机返回为准。

成功:控制台打印 wifiConfigModestatuscatalogability。至少一台是 onlinesleep,且模式里有 SIMCard
兜底:notExistsupport=false,先核对序列号和机型是否在现行平台支持范围内。一直 offline,回到现场查卡和供电。

第四步:核绑定,再一台一台 bindDevice

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

token=管理员 accessToken
deviceId=鱼塘对岸序列号
code=底部8位安全码或改过的设备密码

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

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

成功:bindDevice 返回 code=0;再查一次,isBind=trueisMine=true。再用 listDeviceDetailsByIds 能看到 deviceVersiondeviceStatus
兜底:已经是别人的资产,先让现场从原账号解绑。新机 HTTP 绑不上,按应用开发说明改 SDK 路径。

第五步:bindDeviceLive,本地页播出画

这篇出画接口是 bindDeviceLive。4G 流量金贵,第一次用 streamId=1(标清辅码流)。已经创建过就不要反复创,改打 getLiveStreamInfo。直播地址对外等于公开画面,只给值班内网。

// outdoor-bringup.js
require('dotenv').config();
const { callOpenApi } = require('./openapi-client');

async function bringUp(deviceId, code, channelId = '0') {
  const appId = process.env.IMOU_APP_ID;
  const appSecret = process.env.IMOU_APP_SECRET;
  const token = (await callOpenApi('accessToken', appId, appSecret, {})).accessToken;

  const info = await callOpenApi('unBindDeviceInfo', appId, appSecret, { token, deviceId });
  console.log('[unBindDeviceInfo]', {
    support: info.support,
    deviceExist: info.deviceExist,
    status: info.status,
    wifiConfigMode: info.wifiConfigMode,
    catalog: info.catalog,
  });
  if (!String(info.wifiConfigMode || '').includes('SIMCard')) {
    throw new Error('wifiConfigMode 无 SIMCard,不要按 4G 方案硬绑');
  }

  const bind = await callOpenApi('checkDeviceBindOrNot', appId, appSecret, { token, deviceId });
  if (bind.isBind && !bind.isMine) throw new Error('isMine=false,先从原账号解绑');
  if (!bind.isMine) {
    await callOpenApi('bindDevice', appId, appSecret, { token, deviceId, code });
  }

  let live;
  try {
    live = await callOpenApi('bindDeviceLive', appId, appSecret, {
      token,
      deviceId,
      channelId,
      streamId: 1,
    });
  } catch (e) {
    live = await callOpenApi('getLiveStreamInfo', appId, appSecret, { token, deviceId, channelId });
  }
  const hls = (live.streams || []).map((s) => s.hls).filter(Boolean);
  console.log('[live]', { liveStatus: live.liveStatus, hls, cover: (live.streams || [])[0] && (live.streams || [])[0].coverUrl });
  return { token, info, live };
}

module.exports = { bringUp };

成功:日志里出现 hls 地址,coverUrl 能在浏览器打开一张封面。
兜底:getLiveStreamInfo 为空,说明还没创建成功,先看 bindDeviceLive 的返回,不要先写播放器。sleep 时出不了画,等它醒或按该机能力集看是否支持 closeDormant,不要拿门锁用的唤醒接口套到枪机上。

第六步:接收服务,/preview 出了才算闭环

开放平台吐 HLS,你的服务收成值班页。这是接收侧,不要和签名、绑定揉在一节。

// preview-server.js
require('dotenv').config();
const express = require('express');
const { callOpenApi } = require('./openapi-client');
const SITES = require('./sites.json');

async function token() {
  return (await callOpenApi('accessToken', process.env.IMOU_APP_ID, process.env.IMOU_APP_SECRET, {})).accessToken;
}

const app = express();
app.get('/health', (_req, res) => res.status(200).send('ok'));

app.get('/preview', async (req, res) => {
  const deviceId = req.query.deviceId || Object.keys(SITES)[0];
  const channelId = (SITES[deviceId] && SITES[deviceId].channelId) || '0';
  const tk = await token();
  const detail = await callOpenApi('listDeviceDetailsByIds', process.env.IMOU_APP_ID, process.env.IMOU_APP_SECRET, {
    token: tk,
    deviceList: [{ deviceId, channelId: [channelId] }],
  });
  const live = await callOpenApi('getLiveStreamInfo', process.env.IMOU_APP_ID, process.env.IMOU_APP_SECRET, {
    token: tk,
    deviceId,
    channelId,
  });
  const device = (detail.deviceList || [])[0] || {};
  res.json({
    name: (SITES[deviceId] && SITES[deviceId].name) || deviceId,
    deviceVersion: device.deviceVersion,
    deviceStatus: device.deviceStatus,
    streams: live.streams || [],
  });
});

app.listen(process.env.PORT || 8080);

deviceListlistDeviceDetailsByIds样例传:deviceId + channelId 数组。联调以返回里的 deviceVersion 为准。

按勾,不要跳步。

  1. /health 返回 ok
  2. GET /preview?deviceId=鱼塘对岸序列号 能看到 deviceVersiondeviceStatusstreams[].hls
  3. 浏览器打开一条 HTTPS 的 hlsstatus0),画面是现场,不是黑场。
  4. 抽卡或拔电池,再刷 /preview,该行变成 offline;板子还在、机在睡,应是 sleep,不要打电话。

成功:上面四条都对得上。这就是「值班台出画了」。
兜底:有地址播不出来,先换 streams 里带 https 的那条,并确认本机网络能访问直播域名。单台复核可用 deviceOnline,不要和列表英文枚举混着比。


画面出来之后,再订掉线和回放

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

卡欠费、电池见底要进值班群:用 setMessageCallbackdeviceStatusgetMessageCallback 读回同一条 HTTPS。上下线体的 id 固定是 -1,格式见事件消息格式定义。4G 短抖动可以给 offline 加两三分钟确认窗;sleep 不要进这扇窗。

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

告警进工单、遮挡进群,是下一篇的事。这篇停在「自己的页能出画,抽卡状态会变」。


联调会踩的坑

现象多半是什么先做什么
unBindDeviceInfo 一直 offline卡没激活 / 没费 / 电池空官方 App 能预览再打接口
wifiConfigMode 没有 SIMCard这台不是蜂窝配网不要按 4G 方案硬绑
5Ghz 当成 5G 网那是 Wi-Fi 频段蜂窝只看 SIMCard
HTTP 绑失败新机 / 新固件不走纯 HTTP 绑定应用开发的 SDK 说明
getLiveStreamInfo 是空的还没 bindDeviceLive先创建,再查
有地址、黑场机在 sleep,或播了 HTTP 地址等唤醒;换 HTTPS 的 hls
sleep 当成掉线打电话太阳能 4G 省电offline 分开看
签名对不上标准案例原始串拼错或混了过时算法先对齐 MD5 案例,再谈业务

真正省下的不是再买一块太阳能板

这篇省下的不是多一路镜头、多一块板子,而是值班室不用盯安装师傅的 App,才能回答「鱼塘和对面卡口现在有没有画、掉的是没电还是没卡」。

只在官方 App 里看自家院子、出事了有人盯手机的散户,不必在这里把签名和直播页搭起来。已经有线有电、只差统一台账的园区,去看 bindDevice + listDeviceDetailsByPage 那条线就够了。

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