Comparing version 4.0.4 to 5.0.0
242
index.js
@@ -10,4 +10,4 @@ // Import packages | ||
// Helpers | ||
const Cipher = require('./lib/cipher'); | ||
const Parser = require('./lib/message-parser'); | ||
const {isValidString} = require('./lib/utils'); | ||
const {MessageParser, CommandType} = require('./lib/message-parser'); | ||
@@ -37,3 +37,2 @@ /** | ||
super(); | ||
// Set device to user-passed options | ||
@@ -43,13 +42,16 @@ this.device = {ip, port, id, gwID, key, productKey, version}; | ||
// Check arguments | ||
if (!(this.checkIfValidString(id) || | ||
this.checkIfValidString(ip))) { | ||
if (!(isValidString(id) || | ||
isValidString(ip))) { | ||
throw new TypeError('ID and IP are missing from device.'); | ||
} | ||
if (!this.checkIfValidString(key) || key.length !== 16) { | ||
// Check key | ||
if (!isValidString(this.device.key) || this.device.key.length !== 16) { | ||
throw new TypeError('Key is missing or incorrect.'); | ||
} | ||
// Create cipher from key | ||
this.device.cipher = new Cipher({key, version}); | ||
// Handles encoding/decoding, encrypting/decrypting messages | ||
this.device.parser = new MessageParser({ | ||
key: this.device.key, | ||
version: this.device.version}); | ||
@@ -67,2 +69,7 @@ // Contains array of found devices when calling .find() | ||
this._pingPongPeriod = 10; // Seconds | ||
this._currentSequenceN = 0; | ||
this._resolvers = {}; | ||
this._waitingForSetToResolve = false; | ||
} | ||
@@ -100,5 +107,6 @@ | ||
// Create byte buffer | ||
const buffer = Parser.encode({ | ||
const buffer = this.device.parser.encode({ | ||
data: payload, | ||
commandByte: 10 // 0x0a | ||
commandByte: CommandType.DP_QUERY, | ||
sequenceN: ++this._currentSequenceN | ||
}); | ||
@@ -110,22 +118,13 @@ | ||
// Send request | ||
this._send(buffer).then(() => { | ||
// Runs when data event is emitted | ||
const resolveGet = data => { | ||
// Remove self listener | ||
this.removeListener('data', resolveGet); | ||
if (options.schema === true) { | ||
// Return whole response | ||
resolve(data); | ||
} else if (options.dps) { | ||
// Return specific property | ||
resolve(data.dps[options.dps]); | ||
} else { | ||
// Return first property by default | ||
resolve(data.dps['1']); | ||
} | ||
}; | ||
// Add listener | ||
this.on('data', resolveGet); | ||
this._send(buffer).then(data => { | ||
if (options.schema === true) { | ||
// Return whole response | ||
resolve(data); | ||
} else if (options.dps) { | ||
// Return specific property | ||
resolve(data.dps[options.dps]); | ||
} else { | ||
// Return first property by default | ||
resolve(data.dps['1']); | ||
} | ||
}); | ||
@@ -160,3 +159,3 @@ } catch (error) { | ||
* }}).then(() => console.log('device was changed')) | ||
* @returns {Promise<Boolean>} - returns `true` if the command succeeded | ||
* @returns {Promise<Object>} - returns response from device | ||
*/ | ||
@@ -199,38 +198,18 @@ set(options) { | ||
// Encrypt data | ||
const data = this.device.cipher.encrypt({ | ||
data: JSON.stringify(payload) | ||
}); | ||
// Create MD5 signature | ||
const md5 = this.device.cipher.md5('data=' + data + | ||
'||lpv=' + this.device.version + | ||
'||' + this.device.key); | ||
// Create byte buffer from hex data | ||
const thisData = Buffer.from(this.device.version + md5 + data); | ||
// Encode into packet | ||
const buffer = Parser.encode({ | ||
data: thisData, | ||
commandByte: 7 // 0x07 | ||
const buffer = this.device.parser.encode({ | ||
data: payload, | ||
encrypted: true, // Set commands must be encrypted | ||
commandByte: CommandType.CONTROL, | ||
sequenceN: ++this._currentSequenceN | ||
}); | ||
// Send request and wait for response | ||
this._waitingForSetToResolve = true; | ||
return new Promise((resolve, reject) => { | ||
try { | ||
// Send request | ||
this._send(buffer).then(() => { | ||
// Runs when data event is emitted | ||
const resolveSet = _ => { | ||
// Remove self listener | ||
this.removeListener('data', resolveSet); | ||
this._send(buffer); | ||
// Return true | ||
resolve(true); | ||
}; | ||
// Add listener to data event | ||
this.on('data', resolveSet); | ||
}); | ||
this._setResolver = resolve; | ||
} catch (error) { | ||
@@ -248,3 +227,3 @@ reject(error); | ||
* @param {Buffer} buffer buffer of data | ||
* @returns {Promise<Boolean>} `true` if query was successfully sent | ||
* @returns {Promise<Any>} returned data for request | ||
*/ | ||
@@ -258,7 +237,14 @@ _send(buffer) { | ||
// Retry up to 5 times | ||
return pRetry(async () => { | ||
// Send data | ||
this.client.write(buffer); | ||
return pRetry(() => { | ||
return new Promise((resolve, reject) => { | ||
try { | ||
// Send data | ||
this.client.write(buffer); | ||
return true; | ||
// Add resolver function | ||
this._resolvers[this._currentSequenceN] = data => resolve(data); | ||
} catch (error) { | ||
reject(error); | ||
} | ||
}); | ||
}, {retries: 5}); | ||
@@ -275,5 +261,6 @@ } | ||
// Create byte buffer | ||
const buffer = Parser.encode({ | ||
const buffer = this.device.parser.encode({ | ||
data: Buffer.allocUnsafe(0), | ||
commandByte: 9 // 0x09 | ||
commandByte: CommandType.HEART_BEAT, | ||
sequenceN: ++this._currentSequenceN | ||
}); | ||
@@ -323,11 +310,8 @@ | ||
this.client.on('data', data => { | ||
debug('Received response'); | ||
debug(data.toString('hex')); | ||
debug(`Received data: ${data.toString('hex')}`); | ||
// Response was received, so stop waiting | ||
clearTimeout(this._sendTimeout); | ||
let packets; | ||
let dataRes; | ||
try { | ||
dataRes = Parser.parse(data); | ||
packets = this.device.parser.parse(data); | ||
} catch (error) { | ||
@@ -339,34 +323,8 @@ debug(error); | ||
data = dataRes.data; | ||
packets.forEach(packet => { | ||
debug('Parsed:'); | ||
debug(packet); | ||
if (typeof data === 'object') { | ||
debug('Parsed response data:'); | ||
debug(data); | ||
} else if (typeof data === 'undefined') { | ||
if (dataRes.commandByte === 0x09) { // PONG received | ||
debug('Pong', this.device.ip); | ||
return; | ||
} | ||
if (dataRes.commandByte === 0x07) { // Set succeeded | ||
debug('Set succeeded.'); | ||
return; | ||
} | ||
debug(`Undefined data with command byte ${dataRes.commandByte}`); | ||
} else { // Message is encrypted | ||
data = this.device.cipher.decrypt(data); | ||
debug('Decrypted response data:'); | ||
debug(data); | ||
} | ||
/** | ||
* Emitted when data is returned from device. | ||
* @event TuyaDevice#data | ||
* @property {Object} data received data | ||
* @property {Number} commandByte | ||
* commandByte of result | ||
* (e.g. 7=requested response, 8=proactive update from device) | ||
*/ | ||
this.emit('data', data, dataRes.commandByte); | ||
this._packetHandler.bind(this)(packet); | ||
}); | ||
}); | ||
@@ -443,2 +401,45 @@ | ||
_packetHandler(packet) { | ||
// Response was received, so stop waiting | ||
clearTimeout(this._sendTimeout); | ||
if (packet.commandByte === CommandType.HEART_BEAT) { | ||
debug(`Pong from ${this.device.ip}`); | ||
// Remove resolver | ||
delete this._resolvers[packet.sequenceN]; | ||
return; | ||
} | ||
/** | ||
* Emitted when data is returned from device. | ||
* @event TuyaDevice#data | ||
* @property {Object} data received data | ||
* @property {Number} commandByte | ||
* commandByte of result | ||
* (e.g. 7=requested response, 8=proactive update from device) | ||
* @property {Number} sequenceN the packet sequence number | ||
*/ | ||
this.emit('data', packet.payload, packet.commandByte, packet.sequenceN); | ||
// Status response to SET command | ||
if (packet.sequenceN === 0 && | ||
packet.commandByte === CommandType.STATUS && | ||
this._waitingForSetToResolve) { | ||
this._setResolver(packet.payload); | ||
// Remove resolver | ||
this._setResolver = undefined; | ||
return; | ||
} | ||
// Call data resolver for sequence number | ||
if (packet.sequenceN in this._resolvers) { | ||
this._resolvers[packet.sequenceN](packet.payload); | ||
// Remove resolver | ||
delete this._resolvers[packet.sequenceN]; | ||
} | ||
} | ||
/** | ||
@@ -476,13 +477,2 @@ * Disconnects from the device, use to | ||
/** | ||
* Checks a given input string. | ||
* @private | ||
* @param {String} input input string | ||
* @returns {Boolean} | ||
* `true` if is string and length != 0, `false` otherwise. | ||
*/ | ||
checkIfValidString(input) { | ||
return typeof input === 'string' && input.length > 0; | ||
} | ||
/** | ||
* @deprecated since v3.0.0. Will be removed in v4.0.0. Use find() instead. | ||
@@ -512,4 +502,4 @@ */ | ||
find({timeout = 10, all = false} = {}) { | ||
if (this.checkIfValidString(this.device.id) && | ||
this.checkIfValidString(this.device.ip)) { | ||
if (isValidString(this.device.id) && | ||
isValidString(this.device.ip)) { | ||
// Don't need to do anything | ||
@@ -533,3 +523,3 @@ debug('IP and ID are already both resolved.'); | ||
try { | ||
dataRes = Parser.parse(message); | ||
dataRes = this.device.parser.parse(message)[0]; | ||
} catch (error) { | ||
@@ -541,6 +531,6 @@ debug(error); | ||
debug('UDP data:'); | ||
debug(dataRes.data); | ||
debug(dataRes); | ||
const thisID = dataRes.data.gwId; | ||
const thisIP = dataRes.data.ip; | ||
const thisID = dataRes.payload.gwId; | ||
const thisIP = dataRes.payload.ip; | ||
@@ -554,15 +544,15 @@ // Add to array if it doesn't exist | ||
(this.device.id === thisID || this.device.ip === thisIP) && | ||
dataRes.data) { | ||
dataRes.payload) { | ||
// Add IP | ||
this.device.ip = dataRes.data.ip; | ||
this.device.ip = dataRes.payload.ip; | ||
// Add ID and gwID | ||
this.device.id = dataRes.data.gwId; | ||
this.device.gwID = dataRes.data.gwId; | ||
this.device.id = dataRes.payload.gwId; | ||
this.device.gwID = dataRes.payload.gwId; | ||
// Change product key if neccessary | ||
this.device.productKey = dataRes.data.productKey; | ||
this.device.productKey = dataRes.payload.productKey; | ||
// Change protocol version if necessary | ||
this.device.version = dataRes.data.version; | ||
this.device.version = dataRes.payload.version; | ||
@@ -569,0 +559,0 @@ // Cleanup |
const crypto = require('crypto'); | ||
/** | ||
* Class for encrypting and decrypting payloads. | ||
* Low-level class for encrypting and decrypting payloads. | ||
* @class | ||
* @private | ||
* @param {Object} options | ||
@@ -16,3 +15,3 @@ * @param {String} options.key localKey of cipher | ||
this.key = options.key; | ||
this.version = options.version; | ||
this.version = options.version.toString(); | ||
} | ||
@@ -45,3 +44,3 @@ | ||
* Decrypts data. | ||
* @param {String} data to decrypt | ||
* @param {String|Buffer} data to decrypt | ||
* @returns {Object|String} | ||
@@ -54,3 +53,3 @@ * returns object if data is JSON, else returns string | ||
if (data.indexOf(this.version.toString()) !== -1) { | ||
if (data.indexOf(this.version) === 0) { | ||
// Data has version number and is encoded in base64 | ||
@@ -57,0 +56,0 @@ |
@@ -73,3 +73,3 @@ /* Reverse engineered by kueblc */ | ||
* Computes a Tuya flavored CRC32 | ||
* @param {Iterable} data | ||
* @param {Iterable} bytes | ||
* @returns {Number} Tuya CRC32 | ||
@@ -76,0 +76,0 @@ */ |
@@ -1,2 +0,2 @@ | ||
const debug = require('debug')('TuyAPI:MessageParser'); | ||
const Cipher = require('./cipher'); | ||
const crc = require('./crc'); | ||
@@ -7,140 +7,293 @@ | ||
/** | ||
* Parse a packet from a device into a | ||
* payload and command type | ||
* @param {Buffer} data packet to parse | ||
* @returns {Object} result | ||
* @returns {String|Buffer|Object} result.data decoded data, if available in response | ||
* @returns {Number} result.commandByte command byte from decoded data | ||
* Human-readable definitions | ||
* of command bytes. | ||
* @readonly | ||
* @private | ||
*/ | ||
const CommandType = { | ||
UDP: 0, | ||
AP_CONFIG: 1, | ||
ACTIVE: 2, | ||
BIND: 3, | ||
RENAME_GW: 4, | ||
RENAME_DEVICE: 5, | ||
UNBIND: 6, | ||
CONTROL: 7, | ||
STATUS: 8, | ||
HEART_BEAT: 9, | ||
DP_QUERY: 10, | ||
QUERY_WIFI: 11, | ||
TOKEN_BIND: 12, | ||
CONTROL_NEW: 13, | ||
ENABLE_WIFI: 14, | ||
DP_QUERY_NEW: 16, | ||
SCENE_EXECUTE: 17, | ||
UDP_NEW: 19, | ||
AP_CONFIG_NEW: 20, | ||
LAN_GW_ACTIVE: 240, | ||
LAN_SUB_DEV_REQUEST: 241, | ||
LAN_DELETE_SUB_DEV: 242, | ||
LAN_REPORT_SUB_DEV: 243, | ||
LAN_SCENE: 244, | ||
LAN_PUBLISH_CLOUD_CONFIG: 245, | ||
LAN_PUBLISH_APP_CONFIG: 246, | ||
LAN_EXPORT_APP_CONFIG: 247, | ||
LAN_PUBLISH_SCENE_PANEL: 248, | ||
LAN_REMOVE_GW: 249, | ||
LAN_CHECK_GW_UPDATE: 250, | ||
LAN_GW_UPDATE: 251, | ||
LAN_SET_GW_CHANNEL: 252 | ||
}; | ||
/** | ||
* A complete packet. | ||
* @typedef {Object} Packet | ||
* @property {Buffer|Object|String} payload | ||
* Buffer if hasn't been decoded, object or | ||
* string if it has been | ||
* @property {Buffer} leftover | ||
* bytes adjacent to the parsed packet | ||
* @property {Number} commandByte | ||
* @property {Number} sequenceN | ||
*/ | ||
/** | ||
* Low-level class for parsing packets. | ||
* @class | ||
* @param {Object} options | ||
* @param {String} options.key localKey of cipher | ||
* @param {Number} [options.version=3.1] protocol version | ||
* @example | ||
* const parser = new MessageParser({key: 'xxxxxxxxxxxxxxxx', version: 3.1}) | ||
*/ | ||
function parse(data) { | ||
// Check for length | ||
// At minimum requires: prefix (4), sequence (4), command (4), length (4), | ||
// CRC (4), and suffix (4) for 24 total bytes | ||
// Messages from the device also include return code (4), for 28 total bytes | ||
if (data.length < 24) { | ||
throw new Error('Packet too small. Length: ' + data.length); | ||
} | ||
class MessageParser { | ||
constructor(options) { | ||
// Defaults | ||
options = options ? options : {}; | ||
options.version = options.version ? options.version : '3.1'; | ||
// Check for prefix | ||
const prefix = data.readUInt32BE(0); | ||
if (options.key && options.key.length !== 16) { | ||
throw new TypeError('Incorrect key format'); | ||
} | ||
if (prefix !== 0x000055AA) { | ||
throw new Error('Magic prefix mismatch: ' + data.toString('hex')); | ||
if (options.key && options.version) { | ||
this.cipher = new Cipher(options); | ||
this.key = options.key; | ||
this.version = options.version; | ||
} | ||
} | ||
// Get the command type | ||
const commandByte = data.readUInt32BE(8); | ||
/** | ||
* Parses a Buffer of data containing at least | ||
* one complete packet at the begining of the buffer. | ||
* Will return multiple packets if necessary. | ||
* @param {Buffer} buffer of data to parse | ||
* @returns {Packet} packet of data | ||
*/ | ||
parsePacket(buffer) { | ||
// Check for length | ||
// At minimum requires: prefix (4), sequence (4), command (4), length (4), | ||
// CRC (4), and suffix (4) for 24 total bytes | ||
// Messages from the device also include return code (4), for 28 total bytes | ||
if (buffer.length < 24) { | ||
throw new TypeError(`Packet too short. Length: ${buffer.length}.`); | ||
} | ||
// Get payload size | ||
const payloadSize = data.readUInt32BE(12); | ||
// Check for prefix | ||
const prefix = buffer.readUInt32BE(0); | ||
// Check for payload | ||
if (data.length < HEADER_SIZE + payloadSize) { | ||
throw new Error('Packet missing payload: ' + data.toString('hex')); | ||
} | ||
if (prefix !== 0x000055AA) { | ||
throw new TypeError(`Prefix does not match: ${buffer.toString('hex')}`); | ||
} | ||
// Get the return code, 0 = success | ||
// This field is only present in messages from the devices | ||
// Absent in messages sent to device | ||
const returnCode = data.readUInt32BE(16); | ||
// Check for extra data | ||
let leftover = false; | ||
// Get the payload | ||
// Adjust for messages lacking a return code | ||
let payload; | ||
if (returnCode & 0xFFFFFF00) { | ||
payload = data.slice(HEADER_SIZE, HEADER_SIZE + payloadSize - 8); | ||
} else { | ||
payload = data.slice(HEADER_SIZE + 4, HEADER_SIZE + payloadSize - 8); | ||
} | ||
const suffixLocation = buffer.indexOf('0000AA55', 0, 'hex'); | ||
// Check CRC | ||
const expectedCrc = data.readInt32BE(HEADER_SIZE + payloadSize - 8); | ||
const computedCrc = crc(data.slice(0, payloadSize + 8)); | ||
if (suffixLocation !== buffer.length - 4) { | ||
leftover = buffer.slice(suffixLocation + 4); | ||
buffer = buffer.slice(0, suffixLocation + 4); | ||
} | ||
if (expectedCrc !== computedCrc) { | ||
throw new Error('CRC mismatch: ' + data.toString('hex')); | ||
} | ||
// Check for suffix | ||
const suffix = buffer.readUInt32BE(buffer.length - 4); | ||
// Check for suffix | ||
const suffix = data.readUInt32BE(HEADER_SIZE + payloadSize - 4); | ||
if (suffix !== 0x0000AA55) { | ||
throw new TypeError(`Suffix does not match: ${buffer.toString('hex')}`); | ||
} | ||
if (suffix !== 0x0000AA55) { | ||
throw new Error('Magic suffix mismatch: ' + data.toString('hex')); | ||
// Get sequence number | ||
const sequenceN = buffer.readUInt32BE(4); | ||
// Get payload size | ||
const payloadSize = buffer.readUInt32BE(12); | ||
// Get command byte | ||
const commandByte = buffer.readUInt8(11); | ||
// Check for payload | ||
if (buffer.length - 8 < payloadSize) { | ||
throw new TypeError(`Packet missing payload: payload has length ${payloadSize}.`); | ||
} | ||
// Get the return code, 0 = success | ||
// This field is only present in messages from the devices | ||
// Absent in messages sent to device | ||
const returnCode = buffer.readUInt32BE(16); | ||
// Get the payload | ||
// Adjust for messages lacking a return code | ||
let payload; | ||
if (returnCode & 0xFFFFFF00) { | ||
payload = buffer.slice(HEADER_SIZE, HEADER_SIZE + payloadSize - 8); | ||
} else { | ||
payload = buffer.slice(HEADER_SIZE + 4, HEADER_SIZE + payloadSize - 8); | ||
} | ||
// Check CRC | ||
const expectedCrc = buffer.readInt32BE(HEADER_SIZE + payloadSize - 8); | ||
const computedCrc = crc(buffer.slice(0, payloadSize + 8)); | ||
if (expectedCrc !== computedCrc) { | ||
// eslint-disable-next-line max-len | ||
throw new Error(`CRC mismatch: expected ${expectedCrc}, was ${computedCrc}. ${buffer.toString('hex')}`); | ||
} | ||
return {payload, leftover, commandByte, sequenceN}; | ||
} | ||
// Check for leftovers | ||
if (data.length > HEADER_SIZE + payloadSize) { | ||
debug(data.length - HEADER_SIZE - payloadSize, 'bytes left over'); | ||
// Skip the leftovers for now | ||
/** | ||
* Attempts to decode a given payload into | ||
* an object or string. | ||
* @param {Buffer} data to decode | ||
* @returns {Object|String} | ||
* object if payload is JSON, otherwise string | ||
*/ | ||
getPayload(data) { | ||
if (data.length === 0) { | ||
return false; | ||
} | ||
// Try to parse data as JSON. | ||
// If error, return as string. | ||
try { | ||
data = JSON.parse(data); | ||
} catch (error) { // Data is encrypted | ||
data = data.toString('ascii'); | ||
try { | ||
if (!this.cipher) { | ||
throw new Error('Missing key or version in constructor.'); | ||
} | ||
data = this.cipher.decrypt(data); | ||
} catch (donothing) {} | ||
} | ||
return data; | ||
} | ||
// Attempt to parse data to JSON. | ||
const result = { | ||
commandByte | ||
}; | ||
// It's possible for packets to be valid | ||
// and yet contain no data. | ||
if (payload.length === 0) { | ||
return result; | ||
/** | ||
* Recursive function to parse | ||
* a series of packets. Perfer using | ||
* the parse() wrapper over using this | ||
* directly. | ||
* @private | ||
* @param {Buffer} buffer to parse | ||
* @param {Array} packets that have been parsed | ||
* @returns {Array.<Packet>} array of parsed packets | ||
*/ | ||
parseRecursive(buffer, packets) { | ||
const result = this.parsePacket(buffer); | ||
result.payload = this.getPayload(result.payload); | ||
packets.push(result); | ||
if (result.leftover) { | ||
return this.parseRecursive(result.leftover, packets); | ||
} | ||
return packets; | ||
} | ||
// Try to parse data as JSON. | ||
// If error, return as string. | ||
try { | ||
result.data = JSON.parse(payload); | ||
} catch (error) { // Data is encrypted | ||
result.data = payload.toString('ascii'); | ||
/** | ||
* Given a buffer potentially containing | ||
* multiple packets, this parses and returns | ||
* all of them. | ||
* @param {Buffer} buffer to parse | ||
* @returns {Array.<Packet>} parsed packets | ||
*/ | ||
parse(buffer) { | ||
const packets = this.parseRecursive(buffer, []); | ||
return packets; | ||
} | ||
return result; | ||
} | ||
/** | ||
* Encodes a payload into a | ||
* Tuya-protocol-complient packet. | ||
* @param {Object} options | ||
* @param {Buffer|String|Object} options.data data to encode | ||
* @param {Boolean} options.encrypted whether or not to encrypt the data | ||
* @param {Number} options.commandByte | ||
* command byte of packet (use CommandType definitions) | ||
* @param {Number} [options.sequenceN] optional, sequence number | ||
* @returns {Buffer} | ||
*/ | ||
encode(options) { | ||
// Ensure data is a Buffer | ||
let payload; | ||
/** | ||
* Encode data (usually an object) into | ||
* a protocol-compliant form that a device | ||
* can understand. | ||
* @param {Object} options | ||
* @param {String|Buffer|Object} options.data data to encode | ||
* @param {Number} options.commandByte command byte | ||
* @returns {Buffer} binary payload | ||
*/ | ||
function encode(options) { | ||
// Ensure data is a Buffer | ||
let payload; | ||
// Encrypt data if necessary | ||
if (options.encrypted) { | ||
payload = this.cipher.encrypt({ | ||
data: JSON.stringify(options.data) | ||
}); | ||
if (options.data instanceof Buffer) { | ||
payload = options.data; | ||
} else { | ||
if (typeof options.data === 'string') { | ||
// Create MD5 signature | ||
const md5 = this.cipher.md5('data=' + payload + | ||
'||lpv=' + this.version + | ||
'||' + this.key); | ||
// Create byte buffer from hex data | ||
payload = Buffer.from(this.version + md5 + payload); | ||
} else if (options.data instanceof Buffer) { | ||
payload = options.data; | ||
} else { | ||
payload = JSON.stringify(options.data); | ||
if (typeof options.data !== 'string') { | ||
options.data = JSON.stringify(options.data); | ||
} | ||
payload = Buffer.from(options.data); | ||
} | ||
payload = Buffer.from(payload); | ||
} | ||
// Check command byte | ||
if (Object.values(CommandType).indexOf(options.commandByte) === -1) { | ||
throw new TypeError('Command byte not defined.'); | ||
} | ||
// Ensure commandByte is a Number | ||
if (typeof options.commandByte === 'string') { | ||
options.commandByte = parseInt(options.commandByte, 16); | ||
} | ||
// Allocate buffer with room for payload + 24 bytes for | ||
// prefix, sequence, command, length, crc, and suffix | ||
const buffer = Buffer.alloc(payload.length + 24); | ||
// Allocate buffer with room for payload + 24 bytes for | ||
// prefix, sequence, command, length, crc, and suffix | ||
const buffer = Buffer.alloc(payload.length + 24); | ||
// Add prefix, command, and length | ||
buffer.writeUInt32BE(0x000055AA, 0); | ||
buffer.writeUInt32BE(options.commandByte, 8); | ||
buffer.writeUInt32BE(payload.length + 8, 12); | ||
// Add prefix, command, and length | ||
// Skip sequence number, currently not used | ||
buffer.writeUInt32BE(0x000055AA, 0); | ||
buffer.writeUInt32BE(options.commandByte, 8); | ||
buffer.writeUInt32BE(payload.length + 8, 12); | ||
if (options.sequenceN) { | ||
buffer.writeUInt32BE(options.sequenceN, 4); | ||
} | ||
// Add payload, crc, and suffix | ||
payload.copy(buffer, 16); | ||
buffer.writeInt32BE(crc(payload), payload.length + 16); | ||
buffer.writeUInt32BE(0x0000AA55, payload.length + 20); | ||
// Add payload, crc, and suffix | ||
payload.copy(buffer, 16); | ||
buffer.writeInt32BE(crc(buffer.slice(0, payload.length + 16)), payload.length + 16); | ||
buffer.writeUInt32BE(0x0000AA55, payload.length + 20); | ||
return buffer; | ||
return buffer; | ||
} | ||
} | ||
module.exports = {parse, encode}; | ||
module.exports = {MessageParser, CommandType}; |
{ | ||
"name": "tuyapi", | ||
"version": "4.0.4", | ||
"version": "5.0.0", | ||
"description": "An easy-to-use API for devices that use Tuya's cloud services", | ||
@@ -42,9 +42,10 @@ "main": "index.js", | ||
"p-retry": "4.1.0", | ||
"p-timeout": "3.0.0" | ||
"p-timeout": "3.1.0" | ||
}, | ||
"devDependencies": { | ||
"@tuyapi/stub": "0.1.2", | ||
"@tuyapi/stub": "0.1.3", | ||
"ava": "1.4.1", | ||
"clone": "2.1.2", | ||
"coveralls": "3.0.3", | ||
"delay": "4.2.0", | ||
"documentation": "9.3.1", | ||
@@ -51,0 +52,0 @@ "nyc": "13.3.0", |
@@ -106,7 +106,2 @@ # TuyAPI 🌧 🔌 | ||
## TODO | ||
1. Document details of protocol | ||
2. Figure out correct CRC algorithm | ||
## Contributing | ||
@@ -159,2 +154,3 @@ | ||
- [tuyadump](https://github.com/py60800/tuyadump) a Go project to decode device traffic in real time | ||
- [tuya-mqtt](https://github.com/TheAgentK/tuya-mqtt) a simple MQTT interface for TuyAPI | ||
@@ -161,0 +157,0 @@ |
40029
8
915
8
160
+ Addedp-timeout@3.1.0(transitive)
- Removedp-timeout@3.0.0(transitive)
Updatedp-timeout@3.1.0