UNPKG

node-red-contrib-symi

Version:

Node-RED nodes for smart home device communication with Home Assistant integration, supporting HassKit, Clowire, Alive, LF and complete Symi protocols

225 lines (194 loc) 6.29 kB
/** * 亖米(Symi)协议处理器 * 支持灯光、空调、窗帘、音乐、地暖、新风、场景等设备 * 数据帧格式:15字节基础长度,帧头0x7e,帧尾0x7d,CRC8校验 * * 协议结构: * [帧头][本地地址][数据类型][数据长度][设备类型][品牌ID][设备地址][设备通道] * [房间号][房间类型][房间ID][操作码][操作信息...][CRC8][帧尾] */ const MIN_DATA_LENGTH = 15 // 设备类型定义 const DEVICE_TYPES = { LIGHT: 0x01, // 灯光 HVAC: 0x02, // 空调 CURTAIN: 0x03, // 窗帘 MUSIC: 0x04, // 音乐 HEATING: 0x05, // 地暖 VENTILATION: 0x06, // 新风 SCENE: 0x07 // 场景 }; // 数据类型定义 const DATA_TYPES = { RESPONSE: 0x01, // 应答 QUERY: 0x02, // 查询 SET: 0x03, // 设置 REPORT: 0x04 // 上报 }; const fns = { light: require('./symi/Light'), hvac: require('./symi/HVAC'), curtain: require('./symi/Curtain'), heating: require('./symi/Heating'), ventilation: require('./symi/Ventilation'), scene: require('./symi/Scene') } /** * CRC8校验算法 - 按照协议文档标准实现 * 校验从第1位帧头到操作信息最后一字节 * @param {Buffer} data 待校验数据 */ function CRC8_Check(data) { let crc = 0; let len = data.length; if (len > 2) { let j = len - 2; let ptr = 0; while (j--) { crc ^= data[ptr++]; for (let i = 0; i < 8; i++) { if (crc & 0x80) { crc = (crc << 1) ^ 0x07; } else { crc <<= 1; } } } } return data[len - 2] === (crc & 0xff); } /** * 计算CRC8校验值 - 按照协议文档标准实现 * @param {Buffer} data 数据(不包含CRC和帧尾) */ function CRC8_Calculate(data) { let crc = 0; for (let k = 0; k < data.length; k++) { crc ^= data[k]; for (let i = 0; i < 8; i++) { if (crc & 0x80) { crc = (crc << 1) ^ 0x07; } else { crc <<= 1; } } } return crc & 0xff; } /** * 解析协议数据帧 * @param {Buffer} data 数据帧 */ function parseFrame(data) { if (!data || data.length < MIN_DATA_LENGTH) return null; return { frameHeader: data[0], // 帧头 localAddress: data[1], // 本地地址 dataType: data[2], // 数据类型 dataLength: data[3], // 数据长度 deviceType: data[4], // 设备类型 brandId: data[5], // 品牌ID deviceAddress: data[6], // 设备地址 deviceChannel: data[7], // 设备通道 roomNumber: data[8], // 房间号 roomType: data[9], // 房间类型 roomId: data[10], // 房间ID operationCode: data[11], // 操作码 operationData: data.slice(12, data.length - 2), // 操作信息 crc: data[data.length - 2], // CRC8校验 frameTail: data[data.length - 1] // 帧尾 }; } /** * 构建协议数据帧 * @param {object} frame 帧数据对象 */ function buildFrame(frame) { const operationDataLength = frame.operationData ? frame.operationData.length : 1; const totalLength = 13 + operationDataLength; // 基础12字节 + 操作信息 + CRC + 帧尾 let buffer = Buffer.alloc(totalLength); buffer[0] = 0x7E; // 帧头 buffer[1] = frame.localAddress || 0x01; // 本地地址 buffer[2] = frame.dataType || DATA_TYPES.REPORT; // 数据类型 buffer[3] = totalLength; // 数据长度 buffer[4] = frame.deviceType || 0x01; // 设备类型 buffer[5] = frame.brandId || 0x00; // 品牌ID buffer[6] = frame.deviceAddress || 0x01; // 设备地址 buffer[7] = frame.deviceChannel || 0x00; // 设备通道 buffer[8] = frame.roomNumber || 0x00; // 房间号 buffer[9] = frame.roomType || 0x00; // 房间类型 buffer[10] = frame.roomId || 0x00; // 房间ID buffer[11] = frame.operationCode || 0x00; // 操作码 // 复制操作信息 if (frame.operationData && frame.operationData.length > 0) { frame.operationData.copy(buffer, 12); } else { buffer[12] = 0x00; // 默认操作信息 } // 计算并设置CRC8校验 const crcData = buffer.slice(0, totalLength - 2); buffer[totalLength - 2] = CRC8_Calculate(crcData); // 设置帧尾 buffer[totalLength - 1] = 0x7D; return buffer; } module.exports = { DEVICE_TYPES, DATA_TYPES, /** * 收到tcp数据预处理 * @param {Buffer} data */ handleRece: function (data) { // 校验数据长度 if (data.length < MIN_DATA_LENGTH) return null; // 校验帧头帧尾 if (data[0] !== 0x7E || data[data.length - 1] !== 0x7D) return null; // CRC8校验 if (CRC8_Check(data)) { return data; } return null; }, /** * 处理写入数据 * @param {Buffer|Array|Object} buf */ handle_write: function (buf) { if (Buffer.isBuffer(buf) || Array.isArray(buf)) { // 传统方式:直接处理缓冲区数据 let data = Buffer.from(buf); return buildFrame({ operationData: data }); } else if (typeof buf === 'object') { // 新方式:使用帧对象 return buildFrame(buf); } return null; }, /** * 解析数据帧 * @param {Buffer} data */ parseFrame: parseFrame, /** * 构建数据帧 * @param {object} frame */ buildFrame: buildFrame, /** * 获取对象转缓冲区函数 * @param {string} domain */ getObjToBuf: function (domain) { return fns[domain] ? fns[domain].obj2buf : null; }, /** * 获取缓冲区转对象函数 * @param {string} domain */ getBufToObj: function (domain) { return fns[domain] ? fns[domain].buf2obj : null; } };