11'use strict' ;
22
33// Import packages
4+ const dgram = require ( 'dgram' ) ;
45const forge = require ( 'node-forge' ) ;
5- const recon = require ( '@codetheweb/recon' ) ;
6- const waitUntil = require ( 'wait-until' ) ;
6+ const retryConnect = require ( 'net-retry-connect' ) ;
77
88// Import requests for devices
99const requests = require ( './requests.json' ) ;
@@ -21,21 +21,40 @@ const requests = require('./requests.json');
2121* @param {number } [options.version=3.1] - protocol version
2222*/
2323function TuyaDevice ( options ) {
24- // Init properties
25- this . type = options . type || 'outlet' ;
26- this . ip = options . ip ;
27- this . port = options . port || 6668 ;
28- this . id = options . id ;
29- this . uid = options . uid || '' ;
30- this . key = options . key ;
31- this . version = options . version || 3.1 ;
32-
33- // Create cipher object
34- this . cipher = forge . cipher . createCipher ( 'AES-ECB' , this . key ) ;
35-
36- // Create connection
37- // this.client = new connect({host: this.ip, port: this.port});
38- this . client = recon ( this . ip , this . port , { retryErrors : [ 'ECONNREFUSED' , 'ECONNRESET' ] } ) ;
24+ this . devices = [ ] ;
25+ const needIP = [ ] ;
26+
27+ // If argument is [{id: '', key: ''}]
28+ if ( options . constructor === Array ) {
29+ options . forEach ( function ( device ) {
30+ if ( device . ip === undefined ) {
31+ needIP . push ( device . id ) ;
32+ } else {
33+ this . devices . push ( device ) ;
34+ }
35+ } ) ;
36+
37+ this . discoverDevices ( needIP ) . then ( devices => {
38+ this . devices . push ( devices ) ;
39+ } ) ;
40+ }
41+ // If argument is {id: '', key: ''}
42+ else if ( options . constructor === Object ) {
43+ if ( options . ip === undefined ) {
44+ this . discoverDevices ( options . id ) . then ( device => {
45+ this . devices . push ( device ) ;
46+ } ) ;
47+ } else {
48+ this . devices . push ( {
49+ type : options . type || 'outlet' ,
50+ ip : options . ip ,
51+ port : options . port || 6668 ,
52+ key : options . key ,
53+ cipher : forge . cipher . createCipher ( 'AES-ECB' , options . key ) ,
54+ version : options . version || 3.1
55+ } ) ;
56+ }
57+ }
3958}
4059
4160/**
@@ -123,18 +142,19 @@ TuyaDevice.prototype.setStatus = function (on, callback) {
123142* @returns {Promise<string> } - returned data
124143*/
125144TuyaDevice . prototype . _send = function ( buffer ) {
126- const me = this ;
127145 return new Promise ( ( resolve , reject ) => {
128- // Wait for device to become available
129- waitUntil ( 500 , 40 , ( ) => {
130- return me . client . writable ;
131- } , result => {
132- if ( result === false ) {
133- return reject ( new Error ( 'timeout' ) ) ;
146+ retryConnect . to ( { port : 6668 , host : this . ip , retryOptions : { retries : 5 } } , ( error , client ) => {
147+ if ( error ) {
148+ reject ( error ) ;
134149 }
135- me . client . write ( buffer ) ;
136- me . client . on ( 'data' , data => {
137- return resolve ( data ) ;
150+ client . write ( buffer ) ;
151+
152+ client . on ( 'data' , data => {
153+ client . destroy ( ) ;
154+ resolve ( data ) ;
155+ } ) ;
156+ client . on ( 'error' , error => {
157+ reject ( error ) ;
138158 } ) ;
139159 } ) ;
140160 } ) ;
@@ -184,13 +204,47 @@ TuyaDevice.prototype.getSchema = function () {
184204} ;
185205
186206/**
187- * Breaks connection to device and destroys socket.
188- * @returns {True }
207+ * Attempts to autodiscover devices (i.e. translate device ID to IP).
208+ * @param {Array } IDs - can be a single ID or an array of IDs
209+ * @returns {Promise<object> } devices - discovered devices
210+ */
211+ TuyaDevice . prototype . discoverDevices = function ( ids , callback ) {
212+ // Create new listener if it hasn't already been created
213+ if ( this . listener == undefined ) {
214+ this . listener = dgram . createSocket ( 'udp4' ) ;
215+ this . listener . bind ( 6666 ) ;
216+ }
217+
218+ const discoveredDevices = [ ] ;
219+
220+ // If input is '...' change it to ['...'] for ease of use
221+ if ( typeof ( ids ) === 'string' ) {
222+ ids = [ ids ] ;
223+ }
224+
225+ return new Promise ( ( resolve , reject ) => {
226+ this . listener . on ( 'message' , ( message , info ) => {
227+ if ( discoveredDevices . length < ids . length ) {
228+ if ( ids . includes ( this . _extractJSON ( message ) . gwId ) ) {
229+ discoveredDevices . push ( this . _extractJSON ( message ) ) ;
230+ }
231+ } else { // All IDs have been resolved
232+ resolve ( discoveredDevices ) ;
233+ }
234+ } ) ;
235+ } ) ;
236+ } ;
237+
238+ /**
239+ * Extracts JSON from a raw buffer and returns it as an object.
240+ * @param {Buffer } buffer of data
241+ * @returns {Object } extracted object
189242*/
190- TuyaDevice . prototype . destroy = function ( ) {
191- this . client . end ( ) ;
192- this . client . destroy ( ) ;
193- return true ;
243+ TuyaDevice . prototype . _extractJSON = function ( data ) {
244+ data = data . toString ( ) ;
245+ data = data . slice ( data . indexOf ( '{' ) , data . lastIndexOf ( '"}' ) + 2 ) ;
246+ data = JSON . parse ( data ) ;
247+ return data ;
194248} ;
195249
196250module . exports = TuyaDevice ;
0 commit comments