@tuyapi/link
Advanced tools
Comparing version 0.2.1 to 0.3.0
210
index.js
@@ -1,2 +0,2 @@ | ||
const Cloud = require('@tuyapi/cloud'); | ||
const Cloud = require('@tuyapi/openapi'); | ||
const debug = require('debug')('@tuyapi/link:wizard'); | ||
@@ -6,115 +6,135 @@ const TuyaLink = require('./lib/link.js'); | ||
/** | ||
* A wrapper that combines `@tuyapi/cloud` and | ||
* `(@tuyapi/link).manual` (included in this package) | ||
* to make registration Just Work™️. Exported as | ||
* `(@tuyapi/link).wizard`. | ||
* @class | ||
* @param {Object} options construction options | ||
* @param {String} options.apiKey API key | ||
* @param {String} options.apiSecret API secret | ||
* @param {String} options.email user email | ||
* @param {String} options.password user password | ||
* @param {String} [options.region='AZ'] region (AZ=Americas, AY=Asia, EU=Europe) | ||
* @param {String} [options.timezone='-05:00'] timezone of device | ||
* @example | ||
* // Note: user account does not need to already exist | ||
* const register = new TuyaLink.wizard({key: 'your-api-key', | ||
* secret: 'your-api-secret', | ||
* email: 'example@example.com', | ||
* password: 'example-password'}); | ||
*/ | ||
function TuyaLinkWizard(options) { | ||
// Set to empty object if undefined | ||
options = options ? options : {}; | ||
* A wrapper that combines `@tuyapi/openapi` and | ||
* `(@tuyapi/link).manual` (included in this package) | ||
* to make registration Just Work™️. Exported as | ||
* `(@tuyapi/link).wizard`. | ||
* @class | ||
* @param {Object} options construction options | ||
* @param {String} options.apiKey API key | ||
* @param {String} options.apiSecret API secret | ||
* @param {String} options.schema app schema to register the device under | ||
* @param {String} options.email user email | ||
* @param {String} options.password user password | ||
* @param {String} [options.region='AZ'] region (AZ=Americas, AY=Asia, EU=Europe) | ||
* @param {String} [options.timezone='-05:00'] timezone of device | ||
* @example | ||
* // Note: user account does not need to already exist | ||
* const register = new TuyaLink.wizard({key: 'your-api-key', | ||
* secret: 'your-api-secret', | ||
* email: 'example@example.com', | ||
* password: 'example-password'}); | ||
*/ | ||
class TuyaLinkWizard { | ||
constructor({email, password, region = 'AZ', timezone = '-05:00', apiKey, apiSecret, schema} = {}) { | ||
if (!email || !password) { | ||
throw new Error('Both email and password must be provided'); | ||
} | ||
if (!options.email || !options.password) { | ||
throw new Error('Both email and password must be provided'); | ||
this.email = email; | ||
this.password = password; | ||
this.region = region; | ||
this.timezone = timezone; | ||
// Don't need to check key and secret for correct format as | ||
// tuyapi/openapi already does | ||
this.api = new Cloud({key: apiKey, secret: apiSecret, schema}); | ||
// Construct instance of TuyaLink | ||
this.device = new TuyaLink(); | ||
} | ||
this.email = options.email; | ||
this.password = options.password; | ||
/** | ||
* Logins to Tuya cloud using credentials provided to constructor | ||
* @example | ||
* register.init() | ||
* @returns {Promise<String>} A Promise that contains the session ID | ||
*/ | ||
async init() { | ||
// Register/login user | ||
await this.api.getToken(); | ||
// Set defaults | ||
this.region = options.region ? options.region : 'AZ'; | ||
this.timezone = options.timezone ? options.timezone : '-05:00'; | ||
this.uid = await this.api.putUser({countryCode: '1', username: this.email, password: this.password, usernameType: 2}); | ||
} | ||
// Don't need to check key and secret for correct format as | ||
// tuyapi/cloud already does | ||
this.api = new Cloud({key: options.apiKey, | ||
secret: options.apiSecret, | ||
region: this.region}); | ||
/** | ||
* Links device to WiFi and cloud | ||
* @param {Object} options | ||
* options | ||
* @param {Number} [options.timeout=60] | ||
* how long we should wait for devices to | ||
* connect before throwing an error, in seconds | ||
* @param {String} options.ssid | ||
* the SSID to send to the device | ||
* @param {String} options.wifiPassword | ||
* password for the SSID | ||
* @param {Number} [options.devices=1] | ||
* if linking more than 1 device at a time, | ||
* set to number of devices being linked | ||
* @example | ||
* register.linkDevice({ssid: 'example-ssid', | ||
wifiPassword: 'example-password'}).then(device => { | ||
* console.log(device); | ||
* }); | ||
* @returns {Promise<Object>} A Promise that contains data on device(s) | ||
*/ | ||
async linkDevice({timeout = 60, ssid, wifiPassword = '', devices = 1} = {}) { | ||
if (!ssid) { | ||
throw new Error('SSID must be provided'); | ||
} | ||
// Construct instance of TuyaLink | ||
this.device = new TuyaLink(); | ||
} | ||
try { | ||
const token = await this.api.getDeviceToken({uid: this.uid, timezone: this.timezone}); | ||
/** | ||
* Logins to Tuya cloud using credentials provided to constructor | ||
* @example | ||
* register.init() | ||
* @returns {Promise<String>} A Promise that contains the session ID | ||
*/ | ||
TuyaLinkWizard.prototype.init = function () { | ||
// Register/login user | ||
return this.api.register({email: this.email, password: this.password}); | ||
}; | ||
debug('Token: ', token); | ||
/** | ||
* Links device to WiFi and cloud | ||
* @param {Object} options | ||
* options | ||
* @param {String} options.ssid | ||
* the SSID to send to the device | ||
* @param {String} options.wifiPassword | ||
* password for the SSID | ||
* @param {Number} [options.devices=1] | ||
* if linking more than 1 device at a time, | ||
* set to number of devices being linked | ||
* @example | ||
* register.linkDevice({ssid: 'example-ssid', | ||
wifiPassword: 'example-password'}).then(device => { | ||
* console.log(device); | ||
* }); | ||
* @returns {Promise<Object>} A Promise that contains data on device(s) | ||
*/ | ||
TuyaLinkWizard.prototype.linkDevice = async function (options) { | ||
if (!options.ssid || !options.wifiPassword) { | ||
throw new Error('Both SSID and WiFI password must be provided'); | ||
} | ||
this.device.registerSmartLink({region: this.region, | ||
token: token.token, | ||
secret: token.secret, | ||
ssid, | ||
wifiPassword}); | ||
// Default for options.devices | ||
options.devices = options.devices ? options.devices : 1; | ||
// While UDP packets are being sent, start polling for device | ||
debug('Polling cloud for details on token...'); | ||
try { | ||
const token = await this.api.request({action: 'tuya.m.device.token.create', | ||
data: {timeZone: this.timezone}}); | ||
let waitingForDevices = true; | ||
let lastAPIResponse = {}; | ||
debug('Token: ', token); | ||
const timeoutAt = new Date().getTime() + (timeout * 1000); | ||
this.device.registerSmartLink({region: this.region, | ||
token: token.token, | ||
secret: token.secret, | ||
ssid: options.ssid, | ||
wifiPassword: options.wifiPassword}); | ||
while (waitingForDevices) { | ||
// eslint-disable-next-line no-await-in-loop | ||
lastAPIResponse = await this.api.getDevicesByToken(token.token); | ||
// While UDP packets are being sent, start polling for device | ||
debug('Polling cloud for details on token...'); | ||
debug(`${lastAPIResponse.successDevices.length} devices returned by API.`); | ||
const devices = await this.api.waitForToken({token: token.token, | ||
devices: options.devices}); | ||
debug('Found device(s)!', devices); | ||
if (lastAPIResponse.successDevices.length >= devices) { | ||
waitingForDevices = false; | ||
} | ||
// Stop broadcasting setup data | ||
this.device.abortBroadcastingData(); | ||
// Check for timeout | ||
const now = new Date().getTime(); | ||
// Remove binding on socket | ||
this.device.cleanup(); | ||
if (now > timeoutAt) { | ||
throw new Error('Timed out waiting for devices to connect.'); | ||
} | ||
} | ||
return devices; | ||
} catch (err) { | ||
this.device.cleanup(); | ||
throw err; | ||
const returnedDevices = lastAPIResponse.successDevices; | ||
debug('Found device(s)!', returnedDevices); | ||
// Stop broadcasting setup data | ||
this.device.abortBroadcastingData(); | ||
// Remove binding on socket | ||
this.device.cleanup(); | ||
return returnedDevices; | ||
} catch (error) { | ||
this.device.cleanup(); | ||
throw error; | ||
} | ||
} | ||
}; | ||
} | ||
module.exports = {wizard: TuyaLinkWizard, manual: TuyaLink}; |
621
lib/link.js
@@ -6,177 +6,160 @@ const dgram = require('dgram'); | ||
/** | ||
* A lower level option for linking | ||
* devices. Use only if you're not generating | ||
* a token through `@tuyapi/cloud`. Exported | ||
* as `(@tuyapi/link).manual`. | ||
* Currently has no options, but some may | ||
* be added in the future. | ||
* @class | ||
* @param {Object} options construction options | ||
* @example | ||
* const register = new TuyaLink.manual({}); | ||
*/ | ||
function TuyaLink(options) { | ||
this.abortBroadcasting = false; | ||
* A lower level option for linking | ||
* devices. Use only if you're not generating | ||
* a token through `@tuyapi/cloud`. Exported | ||
* as `(@tuyapi/link).manual`. | ||
* @class | ||
* @example | ||
* const register = new TuyaLink.manual(); | ||
*/ | ||
class TuyaLink { | ||
constructor() { | ||
this.abortBroadcasting = false; | ||
} | ||
return options; | ||
} | ||
/** | ||
* Thin wrapper for this.sendSmartLinkStart() | ||
* and this.sendSmartLinkData(). Unless you | ||
* have a special use case, prefer this method | ||
* over calling this.sendSmartLinkStart() and | ||
* this.sendSmartLinkData() directly. | ||
* @param {Object} options | ||
* options | ||
* @param {String} options.region | ||
* region (see smartLinkEncode() for options) | ||
* @param {String} options.token | ||
* generated token to send | ||
* @param {String} options.secret | ||
* generated secret to send | ||
* @param {String} options.ssid | ||
* SSID to connect to | ||
* @param {String} options.wifiPassword | ||
* password of WiFi | ||
* @example | ||
* device.registerSmartLink({region: 'AZ', | ||
* token: '00000000', | ||
* secret: '0101', | ||
* ssid: 'Example SSID', | ||
* wifiPassword: 'example-password'}).then(() => { | ||
* console.log('Done!'); | ||
* }); | ||
* @returns {Promise<Undefined>} A Promise that resolves when all data has been transmitted | ||
*/ | ||
TuyaLink.prototype.registerSmartLink = function (options) { | ||
/** | ||
* Thin wrapper for this.sendSmartLinkStart() | ||
* and this.sendSmartLinkData(). Unless you | ||
* have a special use case, prefer this method | ||
* over calling this.sendSmartLinkStart() and | ||
* this.sendSmartLinkData() directly. | ||
* @param {Object} options | ||
* options | ||
* @param {String} options.region | ||
* region (see smartLinkEncode() for options) | ||
* @param {String} options.token | ||
* generated token to send | ||
* @param {String} options.secret | ||
* generated secret to send | ||
* @param {String} options.ssid | ||
* SSID to connect to | ||
* @param {String} options.wifiPassword | ||
* password of WiFi | ||
* @example | ||
* device.registerSmartLink({region: 'AZ', | ||
* token: '00000000', | ||
* secret: '0101', | ||
* ssid: 'Example SSID', | ||
* wifiPassword: 'example-password'}).then(() => { | ||
* console.log('Done!'); | ||
* }); | ||
* @returns {Promise<Undefined>} A Promise that resolves when all data has been transmitted | ||
*/ | ||
async registerSmartLink(options) { | ||
// Check arguments | ||
if (options.region.length !== 2) { | ||
throw new Error('Invalid region'); | ||
} | ||
if (options.token.length !== 8) { | ||
throw new Error('Invalid token'); | ||
} | ||
if (options.secret.length !== 4) { | ||
throw new Error('Invalid secret'); | ||
} | ||
if (options.ssid.length > 32) { | ||
throw new Error('Invalid SSID'); | ||
} | ||
if (options.wifiPassword.length > 64) { | ||
throw new Error('Invalid WiFi password'); | ||
} | ||
if (options.region.length !== 2 || !['AZ', 'AY', 'EU'].includes(options.region)) { | ||
throw new Error('Invalid region'); | ||
} | ||
debug('Sending SmartLink initialization packets'); | ||
const that = this; | ||
return new Promise(async (resolve, reject) => { | ||
try { | ||
await this.sendSmartLinkStart(); | ||
debug('Sending SmartLink data packets'); | ||
await this.sendSmartLinkData(that.smartLinkEncode(options)); | ||
debug('Finished sending packets.'); | ||
resolve(); | ||
} catch (err) { | ||
reject(err); | ||
if (options.token.length !== 8) { | ||
throw new Error('Invalid token'); | ||
} | ||
}); | ||
}; | ||
/** | ||
* Transmits start pattern of packets | ||
* (1, 3, 6, 10) 144 times with | ||
* a delay between transmits. | ||
* @returns {Promise<Undefined>} A Promise that resolves when data has been transmitted | ||
*/ | ||
TuyaLink.prototype.sendSmartLinkStart = function () { | ||
const that = this; | ||
return new Promise((async (resolve, reject) => { | ||
try { | ||
/* eslint-disable no-await-in-loop */ | ||
for (let x = 0; x < 144; x++) { | ||
await that._broadcastUDP(1); | ||
await that._broadcastUDP(3); | ||
await that._broadcastUDP(6); | ||
await that._broadcastUDP(10); | ||
await delay((x % 8) + 33); | ||
} | ||
/* eslint-enable no-await-in-loop */ | ||
if (options.secret.length !== 4) { | ||
throw new Error('Invalid secret'); | ||
} | ||
resolve(); | ||
} catch (err) { | ||
reject(err); | ||
if (options.ssid.length > 32) { | ||
throw new Error('Invalid SSID'); | ||
} | ||
})); | ||
}; | ||
/** | ||
* Transmits provided data | ||
* as UDP packet lengths 30 | ||
* times with a delay between | ||
* transmits. | ||
* @param {Array} data of packet lengths to send | ||
* @returns {Promise<Undefined>} A Promise that resolves when data has been transmitted | ||
*/ | ||
TuyaLink.prototype.sendSmartLinkData = function (data) { | ||
const that = this; | ||
if (options.wifiPassword.length > 64) { | ||
throw new Error('Invalid WiFi password'); | ||
} | ||
return new Promise(async (resolve, reject) => { | ||
try { | ||
let delayMs = 0; | ||
debug('Sending SmartLink initialization packets'); | ||
/* eslint-disable no-await-in-loop */ | ||
for (let x = 0; x < 30 && !this.abortBroadcasting; x++) { | ||
if (delayMs > 26) { | ||
delayMs = 6; | ||
} | ||
await this.sendSmartLinkStart(); | ||
debug('Sending SmartLink data packets'); | ||
await this.sendSmartLinkData(this.smartLinkEncode(options)); | ||
debug('Finished sending packets.'); | ||
} | ||
await that._asyncForEach(data, async b => { | ||
await that._broadcastUDP(b); | ||
await delay(delayMs); | ||
}); // 17, 40, 53, 79 | ||
/** | ||
* Transmits start pattern of packets | ||
* (1, 3, 6, 10) 32 times with | ||
* a delay between transmits. | ||
* @returns {Promise<Undefined>} A Promise that resolves when data has been transmitted | ||
*/ | ||
async sendSmartLinkStart() { | ||
const gap = 2; | ||
await delay(200); | ||
delayMs += 3; | ||
} | ||
/* eslint-enable no-await-in-loop */ | ||
/* eslint-disable no-await-in-loop */ // 143/2 | ||
for (let x = 0; x < 32; x++) { | ||
await this._broadcastUDP(1); | ||
await delay(gap); | ||
await this._broadcastUDP(3); | ||
await delay(gap); | ||
await this._broadcastUDP(6); | ||
await delay(gap); | ||
await this._broadcastUDP(10); | ||
await delay(gap); | ||
await this._broadcastUDP(1); | ||
await delay(gap); | ||
await this._broadcastUDP(3); | ||
await delay(gap); | ||
await this._broadcastUDP(6); | ||
await delay(gap); | ||
await this._broadcastUDP(10); | ||
await delay(40);// 70+x%8) | ||
} | ||
/* eslint-enable no-await-in-loop */ | ||
} | ||
this.abortBroadcasting = false; | ||
/** | ||
* Transmits provided data | ||
* as UDP packet lengths 30 | ||
* times with a delay between | ||
* transmits. | ||
* @param {Array} data of packet lengths to send | ||
* @returns {Promise<Undefined>} A Promise that resolves when data has been transmitted | ||
*/ | ||
async sendSmartLinkData(data) { | ||
const gap = 2; | ||
resolve(); | ||
} catch (err) { | ||
reject(err); | ||
/* eslint-disable no-await-in-loop */ | ||
for (let x = 0; x < 10 && !this.abortBroadcasting; x++) { | ||
await delay(160); | ||
await this._asyncForEach(data, async b => { | ||
await this._broadcastUDP(b); | ||
await delay(gap); | ||
}); // 17, 40, 53, 79 | ||
} | ||
}); | ||
}; | ||
/* eslint-enable no-await-in-loop */ | ||
/** | ||
* Aborts broadcasting UDP packets. | ||
*/ | ||
TuyaLink.prototype.abortBroadcastingData = function () { | ||
debug('Aborting broadcast of data...'); | ||
this.abortBroadcasting = true; | ||
}; | ||
this.abortBroadcasting = false; | ||
} | ||
/** | ||
* Encodes data as UDP packet | ||
* lengths. | ||
* @param {Object} options options | ||
* @param {String} options.region | ||
* two-letter region (AZ=Americas, AY=Asia, EU=Europe) | ||
* @param {String} options.token token | ||
* @param {String} options.secret secret | ||
* @param {String} options.ssid SSID | ||
* @param {String} options.wifiPassword | ||
* password of WiFi | ||
* @returns {Array} array of packet lengths | ||
*/ | ||
TuyaLink.prototype.smartLinkEncode = function (options) { | ||
// Convert strings to Buffers | ||
const wifiPasswordBytes = Buffer.from(options.wifiPassword); | ||
const regionTokenSecretBytes = Buffer.from(options.region + | ||
/** | ||
* Aborts broadcasting UDP packets. | ||
*/ | ||
abortBroadcastingData() { | ||
debug('Aborting broadcast of data...'); | ||
this.abortBroadcasting = true; | ||
} | ||
/** | ||
* Encodes data as UDP packet | ||
* lengths. | ||
* @param {Object} options options | ||
* @param {String} options.region | ||
* two-letter region (AZ=Americas, AY=Asia, EU=Europe) | ||
* @param {String} options.token token | ||
* @param {String} options.secret secret | ||
* @param {String} options.ssid SSID | ||
* @param {String} options.wifiPassword | ||
* password of WiFi | ||
* @returns {Array} array of packet lengths | ||
*/ | ||
smartLinkEncode(options) { | ||
// Convert strings to Buffers | ||
const wifiPasswordBytes = Buffer.from(options.wifiPassword); | ||
const regionTokenSecretBytes = Buffer.from(options.region + | ||
options.token + options.secret); | ||
const ssidBytes = Buffer.from(options.ssid); | ||
const ssidBytes = Buffer.from(options.ssid); | ||
// Calculate size of byte array | ||
const rawByteArray = Buffer.alloc(1 + | ||
// Calculate size of byte array | ||
// (must add 1 byte for lengths) | ||
const rawByteArray = Buffer.alloc(1 + | ||
wifiPasswordBytes.length + | ||
@@ -187,202 +170,204 @@ 1 + | ||
let rawByteArrayIndex = 0; | ||
let rawByteArrayIndex = 0; | ||
// Write WiFi password length | ||
rawByteArray.writeInt8(this._getLength(options.wifiPassword), rawByteArrayIndex); | ||
rawByteArrayIndex++; | ||
// Write WiFi password length | ||
rawByteArray.writeInt8(this._getLength(options.wifiPassword), rawByteArrayIndex); | ||
rawByteArrayIndex++; | ||
// Write WiFi password | ||
wifiPasswordBytes.copy(rawByteArray, rawByteArrayIndex); | ||
rawByteArrayIndex += wifiPasswordBytes.length; | ||
// Write WiFi password | ||
wifiPasswordBytes.copy(rawByteArray, rawByteArrayIndex); | ||
rawByteArrayIndex += wifiPasswordBytes.length; | ||
// Write region token secret length | ||
rawByteArray.writeInt8(this._getLength(regionTokenSecretBytes), rawByteArrayIndex); | ||
rawByteArrayIndex++; | ||
// Write region token secret length | ||
rawByteArray.writeInt8(this._getLength(regionTokenSecretBytes), rawByteArrayIndex); | ||
rawByteArrayIndex++; | ||
// Write region token secret bytes | ||
regionTokenSecretBytes.copy(rawByteArray, rawByteArrayIndex); | ||
rawByteArrayIndex += regionTokenSecretBytes.length; | ||
// Write region token secret bytes | ||
regionTokenSecretBytes.copy(rawByteArray, rawByteArrayIndex); | ||
rawByteArrayIndex += regionTokenSecretBytes.length; | ||
// Write WiFi SSID bytes | ||
ssidBytes.copy(rawByteArray, rawByteArrayIndex); | ||
rawByteArrayIndex += ssidBytes.length; | ||
// Write WiFi SSID bytes | ||
ssidBytes.copy(rawByteArray, rawByteArrayIndex); | ||
rawByteArrayIndex += ssidBytes.length; | ||
if (rawByteArray.length !== rawByteArrayIndex) { | ||
throw new Error('Byte buffer filled improperly'); | ||
} | ||
if (rawByteArray.length !== rawByteArrayIndex) { | ||
throw new Error('Byte buffer filled improperly'); | ||
} | ||
// Now, encode above data into packet lengths | ||
const rawDataLengthRoundedUp = this._rounder(rawByteArray.length, 4); | ||
// Now, encode above data into packet lengths | ||
const rawDataLengthRoundedUp = this._rounder(rawByteArray.length, 4); | ||
const encodedData = []; | ||
const encodedData = []; | ||
// First 4 bytes of header | ||
const stringLength = (wifiPasswordBytes.length + | ||
// First 4 bytes of header | ||
const stringLength = (wifiPasswordBytes.length + | ||
regionTokenSecretBytes.length + ssidBytes.length + 2) % 256; | ||
const stringLengthCRC = this._tuyaCRC8([stringLength]); | ||
const stringLengthCRC = this._tuyaCRC8([stringLength]); | ||
// Length encoded into the first two bytes based at 16 and then 32 | ||
encodedData[0] = (stringLength / 16) | 16; | ||
encodedData[1] = (stringLength % 16) | 32; | ||
// Length CRC encoded into the next two bytes based at 46 and 64 | ||
encodedData[2] = (stringLengthCRC / 16) | 48; | ||
encodedData[3] = (stringLengthCRC % 16) | 64; | ||
// Length encoded into the first two bytes based at 16 and then 32 | ||
encodedData[0] = (stringLength / 16) | 16; | ||
encodedData[1] = (stringLength % 16) | 32; | ||
// Length CRC encoded into the next two bytes based at 48 and 64 | ||
encodedData[2] = (stringLengthCRC / 16) | 48; | ||
encodedData[3] = (stringLengthCRC % 16) | 64; | ||
// Rest of data | ||
let encodedDataIndex = 4; | ||
let sequenceCounter = 0; | ||
// Rest of data | ||
let encodedDataIndex = 4; | ||
let sequenceCounter = 0; | ||
for (let x = 0; x < rawDataLengthRoundedUp; x += 4) { | ||
// Build CRC buffer, using data from rawByteArray or 0 values if too long | ||
const crcData = []; | ||
crcData[0] = sequenceCounter++; | ||
crcData[1] = x + 0 < rawByteArray.length ? rawByteArray[x + 0] : 0; | ||
crcData[2] = x + 1 < rawByteArray.length ? rawByteArray[x + 1] : 0; | ||
crcData[3] = x + 2 < rawByteArray.length ? rawByteArray[x + 2] : 0; | ||
crcData[4] = x + 3 < rawByteArray.length ? rawByteArray[x + 3] : 0; | ||
for (let x = 0; x < rawDataLengthRoundedUp; x += 4) { | ||
// Build CRC buffer, using data from rawByteArray or 0 values if too long | ||
const crcData = []; | ||
crcData[0] = sequenceCounter++; | ||
crcData[1] = x + 0 < rawByteArray.length ? rawByteArray[x + 0] : 0; | ||
crcData[2] = x + 1 < rawByteArray.length ? rawByteArray[x + 1] : 0; | ||
crcData[3] = x + 2 < rawByteArray.length ? rawByteArray[x + 2] : 0; | ||
crcData[4] = x + 3 < rawByteArray.length ? rawByteArray[x + 3] : 0; | ||
// Calculate the CRC | ||
const crc = this._tuyaCRC8(crcData); | ||
// Calculate the CRC | ||
const crc = this._tuyaCRC8(crcData); | ||
// Move data to encodedData array | ||
// CRC | ||
encodedData[encodedDataIndex++] = (crc % 128) | 128; | ||
// Sequence number | ||
encodedData[encodedDataIndex++] = (crcData[0] % 128) | 128; | ||
// Data | ||
encodedData[encodedDataIndex++] = (crcData[1] % 256) | 256; | ||
encodedData[encodedDataIndex++] = (crcData[2] % 256) | 256; | ||
encodedData[encodedDataIndex++] = (crcData[3] % 256) | 256; | ||
encodedData[encodedDataIndex++] = (crcData[4] % 256) | 256; | ||
// Move data to encodedData array | ||
// CRC | ||
encodedData[encodedDataIndex++] = (crc % 128) | 128; | ||
// Sequence number | ||
encodedData[encodedDataIndex++] = (crcData[0] % 128) | 128; | ||
// Data | ||
encodedData[encodedDataIndex++] = (crcData[1] % 256) | 256; | ||
encodedData[encodedDataIndex++] = (crcData[2] % 256) | 256; | ||
encodedData[encodedDataIndex++] = (crcData[3] % 256) | 256; | ||
encodedData[encodedDataIndex++] = (crcData[4] % 256) | 256; | ||
} | ||
return encodedData; | ||
} | ||
return encodedData; | ||
}; | ||
/** | ||
* Un-references UDP instance | ||
* so that a script can cleanly | ||
* exit. | ||
*/ | ||
cleanup() { | ||
if (this.udpClient) { | ||
this.udpClient.unref(); | ||
} | ||
} | ||
/** | ||
* Un-references UDP instance | ||
* so that a script can cleanly | ||
* exit. | ||
*/ | ||
TuyaLink.prototype.cleanup = function () { | ||
if (this.udpClient) { | ||
this.udpClient.unref(); | ||
/** | ||
* Returns the length in bytes | ||
* of a string. | ||
* @param {String} str input string | ||
* @returns {Number} length in bytes | ||
* @private | ||
*/ | ||
_getLength(str) { | ||
return Buffer.byteLength(str, 'utf8'); | ||
} | ||
}; | ||
/** | ||
* Returns the length in bytes | ||
* of a string. | ||
* @param {String} str input string | ||
* @returns {Number} length in bytes | ||
* @private | ||
*/ | ||
TuyaLink.prototype._getLength = function (str) { | ||
return Buffer.byteLength(str, 'utf8'); | ||
}; | ||
/** | ||
* Rounds input `x` to the next | ||
* highest multiple of `g`. | ||
* @param {Number} x input number | ||
* @param {Number} g rounding factor | ||
* @returns {Number} rounded result | ||
* @private | ||
*/ | ||
_rounder(x, g) { | ||
return Math.ceil(x / g) * g; | ||
} | ||
/** | ||
* Rounds input `x` to the next | ||
* highest multiple of `g`. | ||
* @param {Number} x input number | ||
* @param {Number} g rounding factor | ||
* @returns {Number} rounded result | ||
* @private | ||
*/ | ||
TuyaLink.prototype._rounder = function (x, g) { | ||
return Math.ceil(x / g) * g; | ||
}; | ||
/** | ||
* Calculates a modified CRC8 | ||
* of a given arary of data. | ||
* @param {Array} p input data | ||
* @returns {Number} CRC result | ||
* @private | ||
*/ | ||
_tuyaCRC8(p) { | ||
let crc = 0; | ||
let i = 0; | ||
const len = p.length; | ||
/** | ||
* Calculates a modified CRC8 | ||
* of a given arary of data. | ||
* @param {Array} p input data | ||
* @returns {Number} CRC result | ||
* @private | ||
*/ | ||
TuyaLink.prototype._tuyaCRC8 = function (p) { | ||
let crc = 0; | ||
let i = 0; | ||
const len = p.length; | ||
while (i < len) { | ||
crc = this._calcrc1Byte(crc ^ p[i]); | ||
i++; | ||
} | ||
while (i < len) { | ||
crc = this._calcrc1Byte(crc ^ p[i]); | ||
i++; | ||
return crc; | ||
} | ||
return crc; | ||
}; | ||
/** | ||
* Calculates a modified | ||
* CRC8 of one byte. | ||
* @param {Number} abyte one byte as an integer | ||
* @returns {Number} resulting CRC8 byte | ||
* @private | ||
*/ | ||
_calcrc1Byte(abyte) { | ||
const crc1Byte = Buffer.alloc(1); | ||
crc1Byte[0] = 0; | ||
/** | ||
* Calculates a modified | ||
* CRC8 of one byte. | ||
* @param {Number} abyte one byte as an integer | ||
* @returns {Number} resulting CRC8 byte | ||
* @private | ||
*/ | ||
TuyaLink.prototype._calcrc1Byte = function (abyte) { | ||
const crc1Byte = Buffer.alloc(1); | ||
crc1Byte[0] = 0; | ||
for (let i = 0; i < 8; i++) { | ||
if (((crc1Byte[0] ^ abyte) & 0x01) > 0) { | ||
crc1Byte[0] ^= 0x18; | ||
crc1Byte[0] >>= 1; | ||
crc1Byte[0] |= 0x80; | ||
} else { | ||
crc1Byte[0] >>= 1; | ||
} | ||
for (let i = 0; i < 8; i++) { | ||
if (((crc1Byte[0] ^ abyte) & 0x01) > 0) { | ||
crc1Byte[0] ^= 0x18; | ||
crc1Byte[0] >>= 1; | ||
crc1Byte[0] |= 0x80; | ||
} else { | ||
crc1Byte[0] >>= 1; | ||
abyte >>= 1; | ||
} | ||
abyte >>= 1; | ||
return crc1Byte[0]; | ||
} | ||
return crc1Byte[0]; | ||
}; | ||
/** | ||
* Broadcasts input number as the | ||
* length of a UDP packet. | ||
* @param {Number} len length of packet to broadcast | ||
* @returns {Promise<Undefined>} | ||
* A Promise that resolves when input has been broadcasted | ||
* @private | ||
*/ | ||
_broadcastUDP(len) { | ||
// Create and bind UDP socket | ||
if (!this.udpClient) { | ||
this.udpClient = dgram.createSocket({type: 'udp4', recvBufferSize: 0, sendBufferSize: 0}); | ||
this.udpClient.on('listening', function () { | ||
this.setBroadcast(true); | ||
}); | ||
this.udpClient.bind(63145); | ||
} | ||
/** | ||
* Broadcasts input number as the | ||
* length of a UDP packet. | ||
* @param {Number} len length of packet to broadcast | ||
* @returns {Promise<Undefined>} | ||
* A Promise that resolves when input has been broadcasted | ||
* @private | ||
*/ | ||
TuyaLink.prototype._broadcastUDP = function (len) { | ||
// Create and bind UDP socket | ||
if (!this.udpClient) { | ||
this.udpClient = dgram.createSocket('udp4'); | ||
this.udpClient.on('listening', function () { | ||
this.setBroadcast(true); | ||
// 0-filled buffer | ||
const message = Buffer.alloc(len); | ||
return new Promise((resolve, reject) => { | ||
this.udpClient.send(message, 0, message.length, 30011, '255.255.255.255', err => { | ||
if (err) { | ||
reject(err); | ||
} | ||
resolve(); | ||
}); | ||
}); | ||
this.udpClient.bind(63145); | ||
} | ||
// 0-filled buffer | ||
const message = Buffer.alloc(len); | ||
return new Promise((resolve, reject) => { | ||
this.udpClient.send(message, 0, message.length, 30011, '255.255.255.255', err => { | ||
if (err) { | ||
reject(err); | ||
} | ||
resolve(); | ||
}); | ||
}); | ||
}; | ||
/** | ||
* A helper that provides an easy | ||
* way to iterate over an array with | ||
* an asynchronous function. | ||
* @param {Array} array input array to iterate over | ||
* @param {function(item, index, array)} callback | ||
* function to call for iterations | ||
* @private | ||
*/ | ||
TuyaLink.prototype._asyncForEach = async function (array, callback) { | ||
for (let index = 0; index < array.length; index++) { | ||
/** | ||
* A helper that provides an easy | ||
* way to iterate over an array with | ||
* an asynchronous function. | ||
* @param {Array} array input array to iterate over | ||
* @param {function(item, index, array)} callback | ||
* function to call for iterations | ||
* @private | ||
*/ | ||
async _asyncForEach(array, callback) { | ||
for (let index = 0; index < array.length; index++) { | ||
// eslint-disable-next-line no-await-in-loop | ||
await callback(array[index], index, array); | ||
await callback(array[index], index, array); | ||
} | ||
} | ||
}; | ||
} | ||
module.exports = TuyaLink; |
{ | ||
"name": "@tuyapi/link", | ||
"version": "0.2.1", | ||
"version": "0.3.0", | ||
"description": "📡 Effortlessly connect devices to WiFi and the cloud", | ||
"main": "index.js", | ||
"files": ["index.js", "lib"], | ||
"scripts": { | ||
@@ -19,8 +20,2 @@ "test": "xo", | ||
"rules": { | ||
"max-len": [ | ||
"error", | ||
{ | ||
"code": 90 | ||
} | ||
], | ||
"indent": [ | ||
@@ -43,3 +38,6 @@ "error", | ||
"tuya", | ||
"udp" | ||
"udp", | ||
"link", | ||
"smartlink", | ||
"wifi" | ||
], | ||
@@ -53,9 +51,9 @@ "author": "“Max <codetheweb@icloud.com> (https://maxisom.me)", | ||
"dependencies": { | ||
"@tuyapi/cloud": "^0.2.2", | ||
"debug": "^3.1.0", | ||
"delay": "^2.0.0" | ||
"@tuyapi/openapi": "^0.1.2", | ||
"debug": "^4.1.1", | ||
"delay": "^4.3.0" | ||
}, | ||
"devDependencies": { | ||
"xo": "^0.21.1" | ||
"xo": "^0.25.3" | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 4 instances in 1 package
Mixed license
License(Experimental) Package contains multiple licenses.
Found 1 instance in 1 package
Non-permissive License
License(Experimental) A license not known to be considered permissive was found.
Found 1 instance in 1 package
0
100
2
19047
5
438
+ Added@tuyapi/openapi@^0.1.2
+ Added@sindresorhus/is@2.1.1(transitive)
+ Added@szmarczak/http-timer@4.0.6(transitive)
+ Added@tuyapi/openapi@0.1.3(transitive)
+ Added@types/cacheable-request@6.0.3(transitive)
+ Added@types/http-cache-semantics@4.0.4(transitive)
+ Added@types/keyv@3.1.4(transitive)
+ Added@types/node@22.9.0(transitive)
+ Added@types/responselike@1.0.3(transitive)
+ Addedcacheable-lookup@2.0.1(transitive)
+ Addedcacheable-request@7.0.4(transitive)
+ Addedclone-response@1.0.3(transitive)
+ Addeddebug@4.3.7(transitive)
+ Addeddecompress-response@5.0.0(transitive)
+ Addeddefer-to-connect@2.0.1(transitive)
+ Addeddelay@4.4.1(transitive)
+ Addedend-of-stream@1.4.4(transitive)
+ Addedget-stream@5.2.0(transitive)
+ Addedgot@10.7.0(transitive)
+ Addedhttp-cache-semantics@4.1.1(transitive)
+ Addedjson-buffer@3.0.1(transitive)
+ Addedkeyv@4.5.4(transitive)
+ Addedlowercase-keys@2.0.0(transitive)
+ Addedmimic-response@2.1.0(transitive)
+ Addednormalize-url@6.1.0(transitive)
+ Addedonce@1.4.0(transitive)
+ Addedp-cancelable@2.1.1(transitive)
+ Addedp-event@4.2.0(transitive)
+ Addedp-timeout@3.2.0(transitive)
+ Addedpump@3.0.2(transitive)
+ Addedresponselike@2.0.1(transitive)
+ Addedto-readable-stream@2.1.0(transitive)
+ Addedtype-fest@0.10.0(transitive)
+ Addedundici-types@6.19.8(transitive)
+ Addedwrappy@1.0.2(transitive)
- Removed@tuyapi/cloud@^0.2.2
- Removed@sindresorhus/is@0.7.0(transitive)
- Removed@tuyapi/cloud@0.2.2(transitive)
- Removedcacheable-request@2.1.4(transitive)
- Removedcharenc@0.0.2(transitive)
- Removedclone-response@1.0.2(transitive)
- Removedcore-util-is@1.0.3(transitive)
- Removedcrypt@0.0.2(transitive)
- Removeddebug@3.2.7(transitive)
- Removeddecode-uri-component@0.2.2(transitive)
- Removeddecompress-response@3.3.0(transitive)
- Removeddelay@2.0.0(transitive)
- Removedfrom2@2.3.0(transitive)
- Removedget-stream@3.0.0(transitive)
- Removedgot@8.3.2(transitive)
- Removedhas-symbol-support-x@1.4.2(transitive)
- Removedhas-to-string-tag-x@1.4.1(transitive)
- Removedhttp-cache-semantics@3.8.1(transitive)
- Removedinherits@2.0.4(transitive)
- Removedinto-stream@3.1.0(transitive)
- Removedis@3.3.0(transitive)
- Removedis-buffer@1.1.6(transitive)
- Removedis-number@4.0.0(transitive)
- Removedis-object@1.0.2(transitive)
- Removedis-plain-obj@1.1.02.1.0(transitive)
- Removedis-retry-allowed@1.2.0(transitive)
- Removedisarray@1.0.0(transitive)
- Removedisurl@1.0.0(transitive)
- Removedjson-buffer@3.0.0(transitive)
- Removedkeyv@3.0.0(transitive)
- Removedkind-of@6.0.3(transitive)
- Removedlowercase-keys@1.0.01.0.1(transitive)
- Removedmath-random@1.0.4(transitive)
- Removedmd5@2.3.0(transitive)
- Removednormalize-url@2.0.1(transitive)
- Removedobject-assign@4.1.1(transitive)
- Removedp-cancelable@0.4.1(transitive)
- Removedp-defer@1.0.0(transitive)
- Removedp-is-promise@1.1.0(transitive)
- Removedp-timeout@2.0.1(transitive)
- Removedpify@3.0.0(transitive)
- Removedprepend-http@2.0.0(transitive)
- Removedprocess-nextick-args@2.0.1(transitive)
- Removedquery-string@5.1.1(transitive)
- Removedrandomatic@3.1.1(transitive)
- Removedreadable-stream@2.3.8(transitive)
- Removedresponselike@1.0.2(transitive)
- Removedsafe-buffer@5.1.25.2.1(transitive)
- Removedsort-keys@2.0.04.2.0(transitive)
- Removedsort-keys-recursive@2.1.10(transitive)
- Removedstrict-uri-encode@1.1.0(transitive)
- Removedstring_decoder@1.1.1(transitive)
- Removedtimed-out@4.0.1(transitive)
- Removedurl-parse-lax@3.0.0(transitive)
- Removedurl-to-options@1.0.1(transitive)
- Removedutil-deprecate@1.0.2(transitive)
Updateddebug@^4.1.1
Updateddelay@^4.3.0