diff --git a/docs/API.md b/docs/API.md index bab0290..de19a60 100644 --- a/docs/API.md +++ b/docs/API.md @@ -8,7 +8,8 @@ Docs - [getStatus](#getstatus) - [setStatus](#setstatus) - [getSchema](#getschema) - - [destroy](#destroy) + - [discoverDevices](#discoverdevices) + - [\_extractJSON](#_extractjson) ## TuyaDevice @@ -48,8 +49,25 @@ Gets control schema from device. Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)>** schema - object of parsed JSON -### destroy +### discoverDevices -Breaks connection to device and destroys socket. +Attempts to autodiscover devices (i.e. translate device ID to IP). -Returns **True** +**Parameters** + +- `ids` +- `callback` +- `IDs` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)** can be a single ID or an array of IDs + +Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)>** devices - discovered devices + +### \_extractJSON + +Extracts JSON from a raw buffer and returns it as an object. + +**Parameters** + +- `data` +- `buffer` **[Buffer](https://nodejs.org/api/buffer.html)** of data + +Returns **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** extracted object diff --git a/index.js b/index.js index 62d9cbd..78ddf22 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,9 @@ 'use strict'; // Import packages +const dgram = require('dgram'); const forge = require('node-forge'); -const recon = require('@codetheweb/recon'); -const waitUntil = require('wait-until'); +const retryConnect = require('net-retry-connect'); // Import requests for devices const requests = require('./requests.json'); @@ -21,21 +21,40 @@ const requests = require('./requests.json'); * @param {number} [options.version=3.1] - protocol version */ function TuyaDevice(options) { - // Init properties - this.type = options.type || 'outlet'; - this.ip = options.ip; - this.port = options.port || 6668; - this.id = options.id; - this.uid = options.uid || ''; - this.key = options.key; - this.version = options.version || 3.1; - - // Create cipher object - this.cipher = forge.cipher.createCipher('AES-ECB', this.key); - - // Create connection - // this.client = new connect({host: this.ip, port: this.port}); - this.client = recon(this.ip, this.port, {retryErrors: ['ECONNREFUSED', 'ECONNRESET']}); + this.devices = []; + const needIP = []; + + // If argument is [{id: '', key: ''}] + if (options.constructor === Array) { + options.forEach(function (device) { + if (device.ip === undefined) { + needIP.push(device.id); + } else { + this.devices.push(device); + } + }); + + this.discoverDevices(needIP).then(devices => { + this.devices.push(devices); + }); + } + // If argument is {id: '', key: ''} + else if (options.constructor === Object) { + if (options.ip === undefined) { + this.discoverDevices(options.id).then(device => { + this.devices.push(device); + }); + } else { + this.devices.push({ + type: options.type || 'outlet', + ip: options.ip, + port: options.port || 6668, + key: options.key, + cipher: forge.cipher.createCipher('AES-ECB', options.key), + version: options.version || 3.1 + }); + } + } } /** @@ -123,18 +142,19 @@ TuyaDevice.prototype.setStatus = function (on, callback) { * @returns {Promise} - returned data */ TuyaDevice.prototype._send = function (buffer) { - const me = this; return new Promise((resolve, reject) => { - // Wait for device to become available - waitUntil(500, 40, () => { - return me.client.writable; - }, result => { - if (result === false) { - return reject(new Error('timeout')); + retryConnect.to({port: 6668, host: this.ip, retryOptions: {retries: 5}}, (error, client) => { + if (error) { + reject(error); } - me.client.write(buffer); - me.client.on('data', data => { - return resolve(data); + client.write(buffer); + + client.on('data', data => { + client.destroy(); + resolve(data); + }); + client.on('error', error => { + reject(error); }); }); }); @@ -184,13 +204,47 @@ TuyaDevice.prototype.getSchema = function () { }; /** -* Breaks connection to device and destroys socket. -* @returns {True} +* Attempts to autodiscover devices (i.e. translate device ID to IP). +* @param {Array} IDs - can be a single ID or an array of IDs +* @returns {Promise} devices - discovered devices +*/ +TuyaDevice.prototype.discoverDevices = function (ids, callback) { + // Create new listener if it hasn't already been created + if (this.listener == undefined) { + this.listener = dgram.createSocket('udp4'); + this.listener.bind(6666); + } + + const discoveredDevices = []; + + // If input is '...' change it to ['...'] for ease of use + if (typeof (ids) === 'string') { + ids = [ids]; + } + + return new Promise((resolve, reject) => { + this.listener.on('message', (message, info) => { + if (discoveredDevices.length < ids.length) { + if (ids.includes(this._extractJSON(message).gwId)) { + discoveredDevices.push(this._extractJSON(message)); + } + } else { // All IDs have been resolved + resolve(discoveredDevices); + } + }); + }); +}; + +/** +* Extracts JSON from a raw buffer and returns it as an object. +* @param {Buffer} buffer of data +* @returns {Object} extracted object */ -TuyaDevice.prototype.destroy = function () { - this.client.end(); - this.client.destroy(); - return true; +TuyaDevice.prototype._extractJSON = function (data) { + data = data.toString(); + data = data.slice(data.indexOf('{'), data.lastIndexOf('"}') + 2); + data = JSON.parse(data); + return data; }; module.exports = TuyaDevice; diff --git a/package-lock.json b/package-lock.json index f4d32eb..bd69179 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "tuyapi", - "version": "1.1.1", + "version": "1.1.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -5542,6 +5542,14 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "net-retry-connect": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/net-retry-connect/-/net-retry-connect-0.1.1.tgz", + "integrity": "sha1-rJUsT1D+5DPcb8tZXzw/GZNsX80=", + "requires": { + "retry": "0.9.0" + } + }, "node-forge": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz", @@ -6530,6 +6538,11 @@ "onetime": "1.1.0" } }, + "retry": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.9.0.tgz", + "integrity": "sha1-b2l+UKDk3cjI9/tUeptg3q1DZ40=" + }, "rimraf": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", diff --git a/package.json b/package.json index 9de4f1c..0ccb13b 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,8 @@ }, "homepage": "https://github.com/codetheweb/tuyapi#readme", "dependencies": { - "@codetheweb/recon": "^1.0.0", - "node-forge": "^0.7.1", - "wait-until": "0.0.2" + "net-retry-connect": "^0.1.1", + "node-forge": "^0.7.1" }, "devDependencies": { "documentation": "^5.3.3",