Skip to content

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.

ItemValue
ProjectCJ188-HEAT-21201
VendorCJ188
ModelCJ188-HEAT
Business Code21201
Integration Modeedgebus-dtu
Protocolcj188

Model catalog

Search for these models in ThinkLink by name or ID.

Model typeNameid_namePlatform model ID
RPC[CJ188 DEBUG]cj188_debug92288175417856005
RPC[CJ188 HEAT ACT] debugcj188_heat_debug79451100201095173
RPC[CJ188 HEAT GET] paracj188_heat_get50496437787561989
RPC[CJ188 HEAT SET] paracj188_heat_set79451423393189893
TemplateCJ188-HEAT-BATTERY79505966550224901
Thing Model[CJ188 HEAT]cj188_heat_2120179250911192420357
Thing Model[CJ188-HEAT-21201资产] 区域聚合cj188_heat_asset_21201119677189950959618

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.

CategorySource FactSource
Protocol/Data> ① thinklink/ ② input/ ③ descriptor.yamlrequirement/requirements.md:5
Protocol/DataVendor CJ188 descriptor.yamlrequirement/requirements.md:11
DeploymentDTU Power Mode Battery descriptor.yamlrequirement/requirements.md:22
Protocol/DataLoRaWAN Class Class Arequirement/requirements.md:23
Deployment> period1..period6 §6.3 §8 DTU 10800mA KC22requirement/requirements.md:27
Unresolved> §4.1 TODO Class A KB-37 ——requirement/requirements.md:28
Unresolved> descriptor.yaml §10 pending confirmationrequirement/requirements.md:29
Deployment- 3 — CJ/T188 KC22 M-Busrequirement/requirements.md:39
Protocol/Data- CJ/T 188-2004 / / §6.3 model1..6requirement/requirements.md:44
Deployment- 1 DTU : 6 M-Busrequirement/requirements.md:45
Deployment- + LoRaWAN /requirement/requirements.md:46
Protocol/Data- supports SET/GET RPCrequirement/requirements.md:52
DeploymentVoltage KC22 descriptor.yamlrequirement/requirements.md:61
UnresolvedPower Consumption / Battery Capacity TODO — input/ KC22 23103 10800mArequirement/requirements.md:62
DeploymentPower-on to Stable Working 1s 1requirement/requirements.md:68
Protocol/DataInterface Type M-Busrequirement/requirements.md:74
Protocol/DataBaud Rate 2400 bps descriptor.yamlrequirement/requirements.md:75
Protocol/Data> shared_attrs.BaudRate 9600 / Checkbit "none" mt_ebrequirement/requirements.md:81
Protocol/Data> descriptor.yaml 2400bps / EVEN ——requirement/requirements.md:82
DeploymentM-Bus A/B + KC22 M-Busrequirement/requirements.md:97
DeploymentDTU-specific wiring notes:requirement/requirements.md:99
Deployment- KC22 M-Bus VCC/GND M-Busrequirement/requirements.md:101
Protocol/DataProtocol Type cj188 CJ/T 188-2004 0x20requirement/requirements.md:110
Protocol/DataDevice Default Address descriptor.yaml default_address: null §6.3 addr1..6requirement/requirements.md:111

Thing Model and RPC Code

RPC: [CJ188 DEBUG]

id_name: cj188_debug · ID: 92288175417856005

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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 ModelFieldTypeUnitName
cj188_heat_21201snrnumberdBSNR
cj188_heat_21201rssinumberdBmRSSI
cj188_heat_21201vbatnumbervBattery Voltage
cj188_heat_21201countnumberCount
cj188_heat_21201dtuEuistringhexDTU EUI
cj188_heat_21201statusstringStatus
cj188_heat_21201tinputnumberInlet Water Temperature
cj188_heat_21201timeoutnumberTimeout
cj188_heat_21201toutputnumberOutlet Water Temperature
cj188_heat_21201sub_addrstringMeter Number
cj188_heat_21201totalflownumberm3Total Flow
cj188_heat_21201totalheatnumberkWhTotal Heat
cj188_heat_21201instantflownumberm3/hInstantaneous Flow
cj188_heat_21201instantheatnumberkWInstantaneous Heat
cj188_heat_asset_21201min_snrnumberdBZone SNR
cj188_heat_asset_21201min_rssinumberdBmZone RSSI
cj188_heat_asset_21201min_vbatnumbervZone Battery Voltage
cj188_heat_asset_21201zone_tinput_avgnumberZone Inlet Water Temperature
cj188_heat_asset_21201zone_toutput_avgnumberZone Outlet Water Temperature
cj188_heat_asset_21201zone_totalflow_sumnumberm3Zone Total Flow
cj188_heat_asset_21201zone_totalheat_sumnumberkWhZone Total Heat
cj188_heat_asset_21201zone_instantflow_sumnumberm3/hZone Instantaneous Flow
cj188_heat_asset_21201zone_instantheat_sumnumberkWZone Instantaneous Heat
cj188_heat_asset_21201online_countnumberZone Online Count

RPC Parameter Definitions

RPCParameterTypeDescriptionDefaultUnit
cj188_debugctrstringCounter0x01
cj188_debugsernumberSerial1
cj188_debugaddrstringModbus Address000000000000
cj188_debuggweuistringGateway EUIauto
cj188_debugdata_tagstringData Tag901Fhex
cj188_debugmeter_typestringMeter Type0x10
cj188_heat_debugaddrstringModbus Address000000000000
cj188_heat_debuggweuistringGateway EUIauto
cj188_heat_debugmodelstringBrand/Model812E1F9012
cj188_heat_setaddr1stringAddress 1
cj188_heat_setaddr2stringAddress 2
cj188_heat_setaddr3stringAddress 3
cj188_heat_setaddr4stringAddress 4
cj188_heat_setaddr5stringAddress 5
cj188_heat_setaddr6stringAddress 6
cj188_heat_setmodel1stringModel 1
cj188_heat_setmodel2stringModel 2
cj188_heat_setmodel3stringModel 3
cj188_heat_setmodel4stringModel 4
cj188_heat_setmodel5stringModel 5
cj188_heat_setmodel6stringModel 6
cj188_heat_setenable1booleanEnable 1
cj188_heat_setenable2booleanEnable 2
cj188_heat_setenable3booleanEnable 3
cj188_heat_setenable4booleanEnable 4
cj188_heat_setenable5booleanEnable 5
cj188_heat_setenable6booleanEnable 6
cj188_heat_setperiod1numberPeriod 1min
cj188_heat_setperiod2numberPeriod 2min
cj188_heat_setperiod3numberPeriod 3min
cj188_heat_setperiod4numberPeriod 4min
cj188_heat_setperiod5numberPeriod 5min
cj188_heat_setperiod6numberPeriod 6min

Trigger Definitions

No project-specific triggers are defined.

Third-Party Platform Subscription

MQTT Topic: /v32/{Organization Account}/tkl/up/telemetry/{eui}

json
{
  "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.