21201-CJ188-HEAT Integration Guide
Sensor Overview
CJ188-HEAT is integrated with ThinkLink through cj188 in edgebus-dtu mode. This guide documents the current deployable artifacts and their source-traceable versions.
| Item | Value |
|---|---|
| Project | CJ188-HEAT-21201 |
| Vendor | CJ188 |
| Model | CJ188-HEAT |
| Business Code | 21201 |
| Integration Mode | edgebus-dtu |
| Protocol | cj188 |
Model catalog
Search for these models in ThinkLink by name or ID.
| Model type | Name | id_name | Platform model ID |
|---|---|---|---|
| RPC | [CJ188 DEBUG] | cj188_debug | 92288175417856005 |
| RPC | [CJ188 HEAT ACT] debug | cj188_heat_debug | 79451100201095173 |
| RPC | [CJ188 HEAT GET] para | cj188_heat_get | 50496437787561989 |
| RPC | [CJ188 HEAT SET] para | cj188_heat_set | 79451423393189893 |
| Template | CJ188-HEAT-BATTERY | 79505966550224901 | |
| Thing Model | [CJ188 HEAT] | cj188_heat_21201 | 79250911192420357 |
| Thing Model | [CJ188-HEAT-21201资产] 区域聚合 | cj188_heat_asset_21201 | 119677189950959618 |
Features and Application Scope
This project covers CJ188-HEAT through cj188. Its capability boundary, fields, and deployable models follow the current descriptor, requirements, and model sources.
Deployment and Data Acquisition
| DTU | KC22 | | Power | battery | | LoRaWAN Class | A | | Interface | M-Bus | | Baud Rate | 2400 |
Telemetry and Register Definitions
The source does not provide a standalone telemetry table; use the current thing models listed in the model inventory.
Parameter Definitions
No project-specific parameters are defined.
Requirements Evidence
The following key facts were extracted from the requirements source; line references support verification.
| Category | Source Fact | Source |
|---|---|---|
| Protocol/Data | > ① thinklink/ ② input/ ③ descriptor.yaml | requirement/requirements.md:5 |
| Protocol/Data | Vendor CJ188 descriptor.yaml | requirement/requirements.md:11 |
| Deployment | DTU Power Mode Battery descriptor.yaml | requirement/requirements.md:22 |
| Protocol/Data | LoRaWAN Class Class A | requirement/requirements.md:23 |
| Deployment | > period1..period6 §6.3 §8 DTU 10800mA KC22 | requirement/requirements.md:27 |
| Unresolved | > §4.1 TODO Class A KB-37 —— | requirement/requirements.md:28 |
| Unresolved | > descriptor.yaml §10 pending confirmation | requirement/requirements.md:29 |
| Deployment | - 3 — CJ/T188 KC22 M-Bus | requirement/requirements.md:39 |
| Protocol/Data | - CJ/T 188-2004 / / §6.3 model1..6 | requirement/requirements.md:44 |
| Deployment | - 1 DTU : 6 M-Bus | requirement/requirements.md:45 |
| Deployment | - + LoRaWAN / | requirement/requirements.md:46 |
| Protocol/Data | - supports SET/GET RPC | requirement/requirements.md:52 |
| Deployment | Voltage KC22 descriptor.yaml | requirement/requirements.md:61 |
| Unresolved | Power Consumption / Battery Capacity TODO — input/ KC22 23103 10800mA | requirement/requirements.md:62 |
| Deployment | Power-on to Stable Working 1s 1 | requirement/requirements.md:68 |
| Protocol/Data | Interface Type M-Bus | requirement/requirements.md:74 |
| Protocol/Data | Baud Rate 2400 bps descriptor.yaml | requirement/requirements.md:75 |
| Protocol/Data | > shared_attrs.BaudRate 9600 / Checkbit "none" mt_eb | requirement/requirements.md:81 |
| Protocol/Data | > descriptor.yaml 2400bps / EVEN —— | requirement/requirements.md:82 |
| Deployment | M-Bus A/B + KC22 M-Bus | requirement/requirements.md:97 |
| Deployment | DTU-specific wiring notes: | requirement/requirements.md:99 |
| Deployment | - KC22 M-Bus VCC/GND M-Bus | requirement/requirements.md:101 |
| Protocol/Data | Protocol Type cj188 CJ/T 188-2004 0x20 | requirement/requirements.md:110 |
| Protocol/Data | Device Default Address descriptor.yaml default_address: null §6.3 addr1..6 | requirement/requirements.md:111 |
Thing Model and RPC Code
RPC: [CJ188 DEBUG]
id_name: cj188_debug · ID: 92288175417856005
import {Utils, BufferTKL} from '#tklHelper';
import {Buffer} from "buffer";
function rpc_script({ device, params, alarms, logger, org_params }) {
let buffer =Buffer.from("FE FE FE 68 20 AA AA AA AA AA AA AA 03 03 81 0A 01 C0 16".replaceAll(" ",""),"hex")
let preamble=3
buffer[preamble+1]=Utils.parseVal(params.meter_type??"0x10")
let addrBuffer=BufferTKL.write(params.addr.padStart(14,"0"),"hexLE7")
addrBuffer.copy(buffer,preamble+2)
buffer[preamble+9]=Utils.parseVal(params.ctr)
buffer[preamble+10]=0x03 //data length
let dataTag=BufferTKL.write(params.data_tag,"hexBE2")
dataTag.copy(buffer,preamble+11,0,2)
buffer[preamble+13]=Utils.parseVal(params.ser)
let crc=Utils.crcSum(buffer.slice(preamble,buffer.length-2),buffer.length-preamble-2)
buffer[buffer.length-2]=crc&0x00FF
if (params?.gweui==undefined){ params.gweui=""}
logger.info("debug cj188")
let msgServerAttrs= {
type:"modifyAttrs",
sleepTimeMs: 0,
dnMsg: {
server_attrs: {
transparent:{
rpc:"cj188_heat_debug",
ms:Date.now(),
params:params
}
}
}
}
let msg= {
sleepTimeMs: 0,
dnMsg: {
"version":"3.0",
"type":"data",
"if":"loraWAN",
"moteeui": device.eui,
"token": new Date().getTime(),
"userdata":{
"confirmed":true,
"fpend":false,
"port":51,
"TxUTCtime":"",
"payload":buffer.toString("base64"),
"dnWaitms":0,
"type":"data",
"intervalms":0,
"specify": {
"gweui": params?.gweui==="auto"?"":params?.gweui
}
}
}
}
return [msgServerAttrs,msg]
}
rpc_script({
device: { server_attrs: {}, shared_attrs: {} },
params: { meter_type:"0x10", addr:"A1A2A3A4A5A6A7", ctr:"0x01", data_tag:"1F90", ser:"0x00" },
alarms: [],
logger: console,
org_params: {}
})RPC: [CJ188 HEAT ACT] debug
id_name: cj188_heat_debug · ID: 79451100201095173
import {Utils, BufferTKL} from '#tklHelper';
import {Buffer} from "buffer";
function rpc_script({ device, params, alarms, logger, org_params }) {
let buffer =Buffer.from("FE FE FE FE FE 68 20 AA AA AA AA AA AA AA 03 03 81 0A 01 C0 16".replaceAll(" ",""),"hex")
let addrBuffer=BufferTKL.write(params.addr.padStart(14,"0"),"hexLE7")
addrBuffer.copy(buffer,7)
let modelBuffer=BufferTKL.write(params.model.padStart(10,"0"),"hexBE5")
modelBuffer.copy(buffer,14,0,5);
let crc=Utils.crcSum(buffer.slice(5,buffer.length-2),buffer.length-7)
buffer[buffer.length-2]=crc&0x00FF
if (params?.gweui==undefined){ params.gweui=""}
let msgServerAttrs= {
type:"modifyAttrs",
sleepTimeMs: 0,
dnMsg: {
server_attrs: {
transparent:{
rpc:"cj188_heat_debug",
ms:Date.now(),
params:params
}
}
}
}
let msg={
sleepTimeMs: 0,
dnMsg: {
"version":"3.0",
"type":params.type,
"if":"loraWAN",
"moteeui": device.eui,
"token": new Date().getTime(),
"userdata":{
"confirmed":true,
"fpend":false,
"port":51,
"TxUTCtime":"",
"payload":buffer.toString("base64"),
"dnWaitms":0,
"type":"data",
"intervalms":0,
"specify": {
"gweui": params?.gweui==="auto"?"":params?.gweui
}
}
}
}
return [msgServerAttrs,msg]
}
rpc_script({
device: { server_attrs: {}, shared_attrs: {} },
params: { addr:"A1A2A3A4A5A6A7", model:"1234567890", type:"read" },
alarms: [],
logger: console,
org_params: {}
})RPC: [CJ188 HEAT GET] para
id_name: cj188_heat_get · ID: 50496437787561989
import {RPCHelper, Utils} from '#tklHelper';
function rpc_script({ device, params, alarms, logger, org_params }) {
let paraDef= {
app_20:{name: "TimeOffset", field_name: "TimeOffset", unit: "", index: 20, type: "uint32le"},
app_34: {name: "retries", field_name: "retries", unit: "", type: "uint8"},
app_38: { name: "pwron_delay", field_name: "pwron_delay", unit: "ms", index: 38, type: "uint16le" },
app_70: {name: "period1", field_name: "period1", unit: "m", type: "uint32LE"},
app_74: {name: "period2", field_name: "period2", unit: "m", type: "uint32LE"},
app_78: {name: "period3", field_name: "period3", unit: "m", type: "uint32LE"},
app_82: {name: "period4", field_name: "period4", unit: "m", type: "uint32LE"},
app_86: {name: "period5", field_name: "period5", unit: "m", type: "uint32LE"},
app_90: {name: "period6", field_name: "period6", unit: "m", type: "uint32LE"},
app_120: {name: "sn1", field_name: "sn1", unit: "", type: "hexLE7"},
app_127: {name: "protocol1", field_name: "protocol1", unit: "", type: "hexBE5"},
app_132: {name: "sn2", field_name: "sn2", unit: "", type: "hexLE7"},
app_139: {name: "protocol2", field_name: "protocol2", unit: "", type: "hexBE5"},
app_144: {name: "sn3", field_name: "sn3", unit: "", type: "hexLE7"},
app_151: {name: "protocol3", field_name: "protocol3", unit: "", type: "hexBE5"},
app_156: {name: "sn4", field_name: "sn4", unit: "", type: "hexLE7"},
app_163: {name: "protocol4", field_name: "protocol4", unit: "", type: "hexBE5"},
app_168: {name: "sn5", field_name: "sn5", unit: "", type: "hexLE7"},
app_175: {name: "protocol5", field_name: "protocol5", unit: "", type: "hexBE5"},
app_180: {name: "sn6", field_name: "sn6", unit: "", type: "hexLE7"},
app_187: {name: "protocol6", field_name: "protocol6", unit: "", type: "hexBE5"},
}
let frames=RPCHelper.buildFrame({
paraDef:paraDef,
params:params});
let msg=RPCHelper.makeMSG({
msgType:Utils.msgType.paras,
device:device,
dnBuffer:frames.readBuffer,
sleepTime:0,
})
return [msg]
}
rpc_script({
device: { server_attrs: {}, shared_attrs: {} },
params: {},
alarms: [],
logger: console,
org_params: {}
})RPC: [CJ188 HEAT SET] para
id_name: cj188_heat_set · ID: 79451423393189893
import {RPCHelper, Utils} from '#tklHelper';
import {Buffer} from "buffer";
function rpc_script({ device, params, alarms, logger, org_params }) {
let classMode = (device && device.shared_attrs && device.shared_attrs.class_mode) || "ClassA";
let sleepMs = classMode === "ClassA" ? 200 : 10000;
let paraDef= {
app_34: {name: "retries", field_name: "retries", unit: "", type: "uint8"},
app_38: { name: "pwron_delay", field_name: "pwron_delay", unit: "ms", index: 38, type: "uint16le" },
app_70: {name: "period1", field_name: "period1", unit: "m", type: "uint32LE"},
app_74: {name: "period2", field_name: "period2", unit: "m", type: "uint32LE"},
app_78: {name: "period3", field_name: "period3", unit: "m", type: "uint32LE"},
app_82: {name: "period4", field_name: "period4", unit: "m", type: "uint32LE"},
app_86: {name: "period5", field_name: "period5", unit: "m", type: "uint32LE"},
app_90: {name: "period6", field_name: "period6", unit: "m", type: "uint32LE"},
app_120: {name: "addr1", field_name: "addr1", unit: "", type: "hexLE7"},
app_127: {name: "model1", field_name: "model1", unit: "", type: "hexBE5"},
app_132: {name: "addr2", field_name: "addr2", unit: "", type: "hexLE7"},
app_139: {name: "model2", field_name: "model2", unit: "", type: "hexBE5"},
app_144: {name: "addr3", field_name: "addr3", unit: "", type: "hexLE7"},
app_151: {name: "model3", field_name: "model3", unit: "", type: "hexBE5"},
app_156: {name: "addr4", field_name: "addr4", unit: "", type: "hexLE7"},
app_163: {name: "model4", field_name: "model4", unit: "", type: "hexBE5"},
app_168: {name: "addr5", field_name: "addr5", unit: "", type: "hexLE7"},
app_175: {name: "model5", field_name: "model5", unit: "", type: "hexBE5"},
app_180: {name: "addr6", field_name: "addr6", unit: "", type: "hexLE7"},
app_187: {name: "model6", field_name: "model6", unit: "", type: "hexBE5"},
}
//logger.info("set paras")
let paraInput = {...params}
for (let i=0;i<6;i++) {
let names={
period:"period"+(i+1).toString(),
enable:"enable"+(i+1).toString(),
addr:"addr"+(i+1).toString(),
model:"model"+(i+1).toString()
}
if (paraInput[names.addr]==null) continue; // KB-31:缺该路子设备参数则跳过,不对 undefined 调 padStart
let period=paraInput[names.period]
let addr=paraInput[names.addr].padStart(14,"0")
let enable=paraInput[names.enable]
let model=paraInput[names.model]
paraInput[names.period]=period*60;
// logger.info("period", {params:{period: period,after:params[names.period]}})
paraInput[names.addr]=addr
if(enable &&(addr==="00000000000000")) enable=false
if(model==="0000000000") enable=false
if (!enable) paraInput[names.period]=86400*365*10
}
paraInput.retries=2
paraInput.pwron_delay=2000
// logger.info("after parames",{params:params})
let frames=RPCHelper.buildFrame({
paraDef:paraDef,
params:paraInput});
let redoBuffer=RPCHelper.redo()
let dnBuffer=Buffer.alloc(frames.writeBuffer.length+frames.readBuffer.length+redoBuffer.length);
frames.writeBuffer.copy(dnBuffer,0)
frames.readBuffer.copy(dnBuffer,frames.writeBuffer.length)
redoBuffer.copy(dnBuffer,frames.writeBuffer.length+frames.readBuffer.length);
// console.log(dnBuffer.toString("hex"))
let msg=RPCHelper.makeMSG({
msgType:Utils.msgType.paras,
device:device,
dnBuffer:dnBuffer,
sleepTime:sleepMs,
})
return [
msg
]
}
rpc_script({
device: { server_attrs: {}, shared_attrs: {} },
params: {},
alarms: [],
logger: console,
org_params: {}
})Thing Model: [CJ188 HEAT]
id_name: cj188_heat_21201 · ID: 79250911192420357
import {PayloadParser, BufferTKL, RPCHelper} from '#tklHelper';
import {Buffer} from "buffer";
function payload_parser(device, msg, thingModelId, noticeAttrs, org_params) {
let port=msg?.userdata?.port || null;
let payload = Buffer.from(msg?.userdata?.payload, "base64");
function parseTelemetry(payload){
const invalidData= {sdevice:null,tdata:null}
if (port!=22||payload[0]!=0x83||payload[1]!=0x33) { return invalidData}
if (payload.length <43) {return invalidData}
let tData={}
let payTKL=new BufferTKL(payload)
let subDevice=payTKL.read(5,"hexLE7")
if (subDevice==="00000000000000") { return invalidData }
let tagData=payTKL.read(12,"hexBE5")
let coefficient=100
if (tagData=="812e901f00") { coefficient=1 } //迈拓
let indexUnit=17
let indexValue=17
let unitFirst=false
if ((payload[indexUnit]===0x05) && (payload[indexUnit+5]===0x17)&&(payload[indexUnit+10]===0x35)&&(payload[indexUnit+15]===0x2C)){
indexValue=18
unitFirst=true
}
tData.totalheat=Number((payTKL.read(indexValue,"bcdLE4")/coefficient).toFixed(2))
tData.instantheat=Number((payTKL.read(indexValue+5,"bcdLE4")/coefficient).toFixed(2))
tData.instantflow=Number((payTKL.read(indexValue+10,"bcdLE4")/(coefficient*100)).toFixed(2))
tData.totalflow=Number((payTKL.read(indexValue+15,"bcdLE4")/coefficient).toFixed(2))
tData.tinput=Number((payTKL.read(indexValue+19+(unitFirst?0:1),"bcdLE3")/100).toFixed(2))
tData.toutput=Number((payTKL.read(indexValue+22+(unitFirst?0:1),"bcdLE3")/100).toFixed(2))
let status =payload.readUInt8(3)
tData.status="正常"
if ((status&0x02)===0x02){
tData={ ...(device.telemetry_data?.[thingModelId] ?? {})}
tData.status="超时"
}
let vbat=payload.readUInt8(4)
tData.vbat=Number(((vbat*1.6)/254 +2.0).toFixed(2))
tData.sub_addr=subDevice
tData.dtuEui=msg?.moteeui
tData.rssi=msg?.gwrx[0].rssi
tData.snr=msg?.gwrx[0].lsnr
return {
sdevice: subDevice,
tdata: tData
}
}
let paraDef= {
app_70: {name: "period1", field_name: "period1", unit: "m", type: "uint32LE"},
app_74: {name: "period2", field_name: "period2", unit: "m", type: "uint32LE"},
app_78: {name: "period3", field_name: "period3", unit: "m", type: "uint32LE"},
app_82: {name: "period4", field_name: "period4", unit: "m", type: "uint32LE"},
app_86: {name: "period5", field_name: "period5", unit: "m", type: "uint32LE"},
app_90: {name: "period6", field_name: "period6", unit: "m", type: "uint32LE"},
app_120: {name: "addr1", field_name: "addr1", unit: "", type: "hexLE7"},
app_127: {name: "model1", field_name: "model1", unit: "", type: "hexBE5"},
app_132: {name: "addr2", field_name: "addr2", unit: "", type: "hexLE7"},
app_139: {name: "model2", field_name: "model2", unit: "", type: "hexBE5"},
app_144: {name: "addr3", field_name: "addr3", unit: "", type: "hexLE7"},
app_151: {name: "model3", field_name: "model3", unit: "", type: "hexBE5"},
app_156: {name: "addr4", field_name: "addr4", unit: "", type: "hexLE7"},
app_163: {name: "model4", field_name: "model4", unit: "", type: "hexBE5"},
app_168: {name: "addr5", field_name: "addr5", unit: "", type: "hexLE7"},
app_175: {name: "model5", field_name: "model5", unit: "", type: "hexBE5"},
app_180: {name: "addr6", field_name: "addr6", unit: "", type: "hexLE7"},
app_187: {name: "model6", field_name: "model6", unit: "", type: "hexBE5"},
}
let pdata={}
let tdata={}
let payParser=new PayloadParser({
device:device,
msg:msg,
paraInfo:paraDef,
})
if (port===214) {
pdata=payParser.paras()
if (pdata!==null){
for (let i=0;i<6;i++){
let names={
period:"period"+(i+1).toString(),
enable:"enable"+(i+1).toString(),
addr:"addr"+(i+1).toString(),
model:"model"+(i+1).toString()
}
let period= pdata[names.period];
let addr= pdata[names.addr];
let mode= pdata[names.mode];
if (period!=undefined){
pdata[names.enable]=true
pdata[names.period]=period/60
if (period>(7*365*86400)) {pdata[names.enable]=false}
if (addr==="00000000000000") {pdata[names.enable]=false}
if (mode==="0000000000") {pdata[names.enable]=false}
}
}
}
tdata=null
}else {
tdata= parseTelemetry(payload)
if (tdata && tdata.tdata) { // 无效/空帧时 parseTelemetry 可能回 tdata.tdata=null;server_attrs 也可能未设(新设备)→ 全部加守卫,不再抛异常
tdata.tdata.count= (device.server_attrs?.["count_"+tdata?.sdevice]?? 0 )
tdata.tdata.timeout= (device.server_attrs?.["timeout_"+tdata?.sdevice]?? 0 )
tdata.tdata.rbpp=device.server_attrs?.["jf_"+tdata?.sdevice]?.rbpp
tdata.tdata.fwbh=device.server_attrs?.["jf_"+tdata?.sdevice]?.fwbh
tdata.tdata.prefix=device.server_attrs?.["jf_"+tdata?.sdevice]?.prefix
}
pdata=null
}
let result = {
sub_device: tdata===null?null:tdata.sdevice,
telemetry_data: tdata===null?null:tdata.tdata,
server_attrs: {count:tdata?.tdata?.count},
shared_attrs: pdata
};
// ── rpcHook (设备对接通用规则) ──
if (typeof RPCHelper.buildRpcHookAction === "function") {
const __a = RPCHelper.buildRpcHookAction({ device, msg, thingModelId, idName: "cj188_heat_21201", name: "[CJ188 HEAT]", parsed: { telemetry_data: result.telemetry_data, server_attrs: result.server_attrs, shared_attrs: result.shared_attrs } });
if (__a) { result.actions = result.actions || []; result.actions.push(__a); }
}
return result;
}
console.log(payload_parser(
{ telemetry_data: {}, server_attrs: {} },
{ userdata: { port: 11, payload: "" } },
"1",
{ telemetry_data: true },
{}
))Thing Model: cj188_heat_asset_21201
id_name: cj188_heat_asset_21201 · ID: 119677189950959618
import {PayloadParser, RPCHelper} from '#tklHelper';
// 资产(VIRTUAL)聚合物模型 — 通用 agg_spec 驱动聚合器,见 docs/references/workflow/asset-aggregation-model.md
// device=资产, msg=源设备快照, thingModelId=本物模型ID, noticeAttrs=变更标记。
// 聚合规格在资产 server_attrs.agg_spec:min_snr=取最小(snr);min_rssi=取最小(rssi);min_vbat=取最小(vbat);zone_tinput_avg=空间均值(tinput);zone_toutput_avg=空间均值(toutput);zone_totalflow_sum=谨慎累加(totalflow);zone_totalheat_sum=谨慎累加(totalheat);zone_instantflow_sum=时点累加(instantflow);zone_instantheat_sum=时点累加(instantheat);online_count=在线数(—)
function payload_parser(device, msg, thingModelId, noticeAttrs) {
if (!noticeAttrs || !noticeAttrs.telemetry_data) {
return { telemetry_data: null, server_attrs: null, shared_attrs: null };
}
const sa = device?.server_attrs || {};
const spec = sa.agg_spec || {};
const members = { ...(sa.members || {}) };
const staleMs = sa.stale_ms != null ? sa.stale_ms : 600000;
const now = (typeof Date !== "undefined" && Date.now) ? Date.now() : null;
// 合并源设备各物模型的最新快照为一个扁平对象
const srcTd = {};
const all = msg?.telemetry_data || {};
for (const k in all) { Object.assign(srcTd, all[k]); }
// 收集 spec 引用到的全部源字段,把本帧带到的值并进该成员快照
const want = {};
for (const out in spec) { if (spec[out].src) want[spec[out].src] = true; }
const got = {}; let touched = false;
for (const f in want) { if (srcTd[f] !== undefined && srcTd[f] !== null) { got[f] = srcTd[f]; touched = true; } }
if (touched && msg && msg.eui) {
const prev = (members[msg.eui] && members[msg.eui].v) || {};
const seq = (members[msg.eui] && members[msg.eui].ts) || 0;
members[msg.eui] = { v: Object.assign({}, prev, got), ts: now != null ? now : seq + 1 };
}
const euis = Object.keys(members);
const fresh = euis.filter(function (e) {
if (now == null || staleMs <= 0) return true;
const ts = members[e].ts;
return ts != null && ts <= now && (now - ts) <= staleMs;
});
const num = function (x) { return (x != null && !isNaN(x)) ? Number(x) : null; };
const out = {};
for (const field in spec) {
const op = spec[field].op, src = spec[field].src;
if (op === "count_online") { out[field] = fresh.length; continue; }
const pool = (op === "sum_cumul") ? euis : fresh;
const vals = pool.map(function (e) { return num(members[e].v && members[e].v[src]); }).filter(function (x) { return x != null; });
if (op === "avg") out[field] = vals.length ? Number((vals.reduce(function (a, b) { return a + b; }, 0) / vals.length).toFixed(2)) : null;
else if (op === "sum_snap" || op === "sum_cumul") out[field] = vals.length ? Number(vals.reduce(function (a, b) { return a + b; }, 0).toFixed(3)) : null;
else if (op === "max") out[field] = vals.length ? Math.max.apply(null, vals) : null;
else if (op === "min") out[field] = vals.length ? Math.min.apply(null, vals) : null;
else if (op === "or") out[field] = pool.some(function (e) { const x = members[e].v && members[e].v[src]; return x != null && x != 0; }) ? 1 : 0;
else if (op === "count_on") out[field] = pool.filter(function (e) { const x = members[e].v && members[e].v[src]; return x != null && x != 0; }).length;
else out[field] = null;
}
let result = { telemetry_data: out, server_attrs: { members: members }, shared_attrs: null };
// ── rpcHook (设备对接通用规则) ──
if (typeof RPCHelper.buildRpcHookAction === "function") {
const __a = RPCHelper.buildRpcHookAction({ device, msg, thingModelId, idName: "cj188_heat_asset_21201", name: "[CJ188-HEAT-21201资产] 区域聚合", parsed: { telemetry_data: result.telemetry_data, server_attrs: result.server_attrs, shared_attrs: result.shared_attrs } });
if (__a) { result.actions = result.actions || []; result.actions.push(__a); }
}
return result;
}
// --- local debug ---(两台成员先后上报;平台会把返回的 members 持久化回 device,这里手动模拟)
const agg_spec = {
"min_snr": {
"op": "min",
"src": "snr"
},
"min_rssi": {
"op": "min",
"src": "rssi"
},
"min_vbat": {
"op": "min",
"src": "vbat"
},
"zone_tinput_avg": {
"op": "avg",
"src": "tinput"
},
"zone_toutput_avg": {
"op": "avg",
"src": "toutput"
},
"zone_totalflow_sum": {
"op": "sum_cumul",
"src": "totalflow"
},
"zone_totalheat_sum": {
"op": "sum_cumul",
"src": "totalheat"
},
"zone_instantflow_sum": {
"op": "sum_snap",
"src": "instantflow"
},
"zone_instantheat_sum": {
"op": "sum_snap",
"src": "instantheat"
},
"online_count": {
"op": "count_online",
"src": ""
}
};
const device = { name: "CJ188-HEAT-21201资产-1", server_attrs: { agg_spec, stale_ms: 1800000, members: {} } };
const r1 = payload_parser(device, { eui: "MEMBER_A", telemetry_data: { m1: {"snr":1,"rssi":1,"vbat":1,"tinput":1,"toutput":1,"totalflow":1,"totalheat":1,"instantflow":1,"instantheat":1} } }, "tmid", { telemetry_data: true });
device.server_attrs.members = r1.server_attrs.members; // 平台持久化
console.log("成员A上报后:", JSON.stringify(r1.telemetry_data));
const r2 = payload_parser(device, { eui: "MEMBER_B", telemetry_data: { m1: {"snr":2,"rssi":2,"vbat":2,"tinput":2,"toutput":2,"totalflow":2,"totalheat":2,"instantflow":2,"instantheat":2} } }, "tmid", { telemetry_data: true });
device.server_attrs.members = r2.server_attrs.members;
console.log("成员B上报后(2台聚合):", JSON.stringify(r2.telemetry_data));
void PayloadParser;Thing Model Field Definitions
| Thing Model | Field | Type | Unit | Name |
|---|---|---|---|---|
| cj188_heat_21201 | snr | number | dB | SNR |
| cj188_heat_21201 | rssi | number | dBm | RSSI |
| cj188_heat_21201 | vbat | number | v | Battery Voltage |
| cj188_heat_21201 | count | number | Count | |
| cj188_heat_21201 | dtuEui | string | hex | DTU EUI |
| cj188_heat_21201 | status | string | Status | |
| cj188_heat_21201 | tinput | number | ℃ | Inlet Water Temperature |
| cj188_heat_21201 | timeout | number | Timeout | |
| cj188_heat_21201 | toutput | number | ℃ | Outlet Water Temperature |
| cj188_heat_21201 | sub_addr | string | Meter Number | |
| cj188_heat_21201 | totalflow | number | m3 | Total Flow |
| cj188_heat_21201 | totalheat | number | kWh | Total Heat |
| cj188_heat_21201 | instantflow | number | m3/h | Instantaneous Flow |
| cj188_heat_21201 | instantheat | number | kW | Instantaneous Heat |
| cj188_heat_asset_21201 | min_snr | number | dB | Zone SNR |
| cj188_heat_asset_21201 | min_rssi | number | dBm | Zone RSSI |
| cj188_heat_asset_21201 | min_vbat | number | v | Zone Battery Voltage |
| cj188_heat_asset_21201 | zone_tinput_avg | number | ℃ | Zone Inlet Water Temperature |
| cj188_heat_asset_21201 | zone_toutput_avg | number | ℃ | Zone Outlet Water Temperature |
| cj188_heat_asset_21201 | zone_totalflow_sum | number | m3 | Zone Total Flow |
| cj188_heat_asset_21201 | zone_totalheat_sum | number | kWh | Zone Total Heat |
| cj188_heat_asset_21201 | zone_instantflow_sum | number | m3/h | Zone Instantaneous Flow |
| cj188_heat_asset_21201 | zone_instantheat_sum | number | kW | Zone Instantaneous Heat |
| cj188_heat_asset_21201 | online_count | number | Zone Online Count |
RPC Parameter Definitions
| RPC | Parameter | Type | Description | Default | Unit |
|---|---|---|---|---|---|
| cj188_debug | ctr | string | Counter | 0x01 | |
| cj188_debug | ser | number | Serial | 1 | |
| cj188_debug | addr | string | Modbus Address | 000000000000 | |
| cj188_debug | gweui | string | Gateway EUI | auto | |
| cj188_debug | data_tag | string | Data Tag | 901F | hex |
| cj188_debug | meter_type | string | Meter Type | 0x10 | |
| cj188_heat_debug | addr | string | Modbus Address | 000000000000 | |
| cj188_heat_debug | gweui | string | Gateway EUI | auto | |
| cj188_heat_debug | model | string | Brand/Model | 812E1F9012 | |
| cj188_heat_set | addr1 | string | Address 1 | — | |
| cj188_heat_set | addr2 | string | Address 2 | — | |
| cj188_heat_set | addr3 | string | Address 3 | — | |
| cj188_heat_set | addr4 | string | Address 4 | — | |
| cj188_heat_set | addr5 | string | Address 5 | — | |
| cj188_heat_set | addr6 | string | Address 6 | — | |
| cj188_heat_set | model1 | string | Model 1 | — | |
| cj188_heat_set | model2 | string | Model 2 | — | |
| cj188_heat_set | model3 | string | Model 3 | — | |
| cj188_heat_set | model4 | string | Model 4 | — | |
| cj188_heat_set | model5 | string | Model 5 | — | |
| cj188_heat_set | model6 | string | Model 6 | — | |
| cj188_heat_set | enable1 | boolean | Enable 1 | — | |
| cj188_heat_set | enable2 | boolean | Enable 2 | — | |
| cj188_heat_set | enable3 | boolean | Enable 3 | — | |
| cj188_heat_set | enable4 | boolean | Enable 4 | — | |
| cj188_heat_set | enable5 | boolean | Enable 5 | — | |
| cj188_heat_set | enable6 | boolean | Enable 6 | — | |
| cj188_heat_set | period1 | number | Period 1 | — | min |
| cj188_heat_set | period2 | number | Period 2 | — | min |
| cj188_heat_set | period3 | number | Period 3 | — | min |
| cj188_heat_set | period4 | number | Period 4 | — | min |
| cj188_heat_set | period5 | number | Period 5 | — | min |
| cj188_heat_set | period6 | number | Period 6 | — | min |
Trigger Definitions
No project-specific triggers are defined.
Third-Party Platform Subscription
MQTT Topic: /v32/{Organization Account}/tkl/up/telemetry/{eui}
{
"eui": "6353012af10a9331",
"active_time": "2026-07-13T08:35:48.000Z",
"thingModelId": "Use the platform model ID from the model inventory",
"thingModelIdName": "Use the current telemetry model id_name",
"telemetry_data": {
"snr": 13.5,
"rssi": -51,
"battery": 3.37
}
}Template Selection and Verification
Search ThinkLink by project name, business code, or the id_name and ID values in the model inventory. After deployment, use real uplinks to verify fields, units, RPCs, and trigger behavior.
Supplementary Notes
Requirements and decisions: requirement/requirements.md.