44const dgram = require ( 'dgram' ) ;
55const forge = require ( 'node-forge' ) ;
66const retryConnect = require ( 'net-retry-connect' ) ;
7+ const stringOccurrence = require ( 'string-occurrence' ) ;
78
89// Import requests for devices
910const requests = require ( './requests.json' ) ;
@@ -22,68 +23,116 @@ const requests = require('./requests.json');
2223*/
2324function TuyaDevice ( options ) {
2425 this . devices = [ ] ;
25- const needIP = [ ] ;
2626
2727 // If argument is [{id: '', key: ''}]
2828 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- } ) ;
29+ this . devices = options ;
4030 }
31+
4132 // If argument is {id: '', key: ''}
4233 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- }
34+ this . devices = [ options ] ;
35+ }
36+
37+ // standardize devices array
38+ for ( var i = 0 ; i < this . devices . length ; i ++ ) {
39+ if ( this . devices [ i ] . type === undefined ) { this . devices [ i ] . type = 'outlet' ; }
40+ if ( this . devices [ i ] . port === undefined ) { this . devices [ i ] . port = 6668 ; }
41+ if ( this . devices [ i ] . version === undefined ) { this . devices [ i ] . version = 3.1 ; }
42+
43+ // create cipher from key
44+ this . devices [ i ] . cipher = forge . cipher . createCipher ( 'AES-ECB' , this . devices [ i ] . key ) ;
5745 }
5846}
5947
6048/**
61- * Gets the device's current status.
49+ * Resolves IDs stored in class to IPs.
50+ * @returns {Promise<Boolean> } - true if IPs were found and devices are ready to be used
51+ */
52+ TuyaDevice . prototype . resolveIds = function ( ) {
53+ // Create new listener if it hasn't already been created
54+ if ( this . listener == undefined ) {
55+ this . listener = dgram . createSocket ( 'udp4' ) ;
56+ this . listener . bind ( 6666 ) ;
57+ }
58+
59+ // find devices that need an IP
60+ var needIP = [ ] ;
61+ for ( var i = 0 ; i < this . devices . length ; i ++ ) {
62+ if ( this . devices [ i ] . ip == undefined ) {
63+ needIP . push ( this . devices [ i ] . id ) ;
64+ }
65+ }
66+
67+ // todo: add timeout for when IP cannot be found, then reject(with error)
68+ // add IPs to devices in array and return true
69+ return new Promise ( ( resolve , reject ) => {
70+ this . listener . on ( 'message' , ( message , info ) => {
71+ let thisId = this . _extractJSON ( message ) . gwId ;
72+
73+ if ( needIP . length > 0 ) {
74+ if ( needIP . includes ( thisId ) ) {
75+ var deviceIndex = this . devices . findIndex ( device => {
76+ if ( device . id === thisId ) { return true ; }
77+ } ) ;
78+
79+ this . devices [ deviceIndex ] . ip = this . _extractJSON ( message ) . ip ;
80+
81+ needIP . splice ( needIP . indexOf ( thisId ) , 1 ) ;
82+ }
83+ }
84+ else { // all devices have been resolved
85+ this . listener . removeAllListeners ( ) ;
86+ resolve ( true ) ;
87+ }
88+ } )
89+ } ) ;
90+ } ;
91+
92+ /**
93+ * Gets the device's current status. Defaults to returning only the first 'dps', but by setting {schema: true} you can get everything.
94+ * @param {string } ID - optional, ID of device. Defaults to first device.
6295* @param {function(error, result) } callback
6396*/
64- TuyaDevice . prototype . getStatus = function ( callback ) {
97+ TuyaDevice . prototype . get = function ( options ) {
98+ var currentDevice ;
99+
100+ // If no ID is provided
101+ if ( options === undefined || options . id === undefined ) {
102+ currentDevice = this . devices [ 0 ] ; // use first device in array
103+ }
104+ else { // otherwise
105+ // find the device by id in this.devices
106+ let index = this . devices . findIndex ( device => {
107+ if ( device . id === options . id ) { return true ; }
108+ } ) ;
109+ currentDevice = this . devices [ index ]
110+ }
111+
65112 // Add data to command
66- if ( 'gwId' in requests [ this . type ] . status . command ) {
67- requests [ this . type ] . status . command . gwId = this . id ;
113+ if ( 'gwId' in requests [ currentDevice . type ] . status . command ) {
114+ requests [ currentDevice . type ] . status . command . gwId = currentDevice . id ;
68115 }
69- if ( 'devId' in requests [ this . type ] . status . command ) {
70- requests [ this . type ] . status . command . devId = this . id ;
116+ if ( 'devId' in requests [ currentDevice . type ] . status . command ) {
117+ requests [ currentDevice . type ] . status . command . devId = currentDevice . id ;
71118 }
72119
73120 // Create byte buffer from hex data
74- const thisData = Buffer . from ( JSON . stringify ( requests [ this . type ] . status . command ) ) ;
75- const buffer = this . _constructBuffer ( thisData , 'status' ) ;
121+ const thisData = Buffer . from ( JSON . stringify ( requests [ currentDevice . type ] . status . command ) ) ;
122+ const buffer = this . _constructBuffer ( currentDevice . type , thisData , 'status' ) ;
76123
77- this . _send ( buffer ) . then ( data => {
78- // Extract returned JSON
79- try {
80- data = data . toString ( ) ;
81- data = data . slice ( data . indexOf ( '{' ) , data . lastIndexOf ( '}' ) + 1 ) ;
82- data = JSON . parse ( data ) ;
83- return callback ( null , data . dps [ '1' ] ) ;
84- } catch ( err ) {
85- return callback ( err , null ) ;
86- }
124+ return new Promise ( ( resolve , reject ) => {
125+ this . _send ( currentDevice . ip , buffer ) . then ( data => {
126+ // Extract returned JSON
127+ data = this . _extractJSON ( data ) ;
128+
129+ if ( options != undefined && options . schema == true ) {
130+ resolve ( data ) ;
131+ }
132+ else {
133+ resolve ( data . dps [ '1' ] )
134+ }
135+ } ) ;
87136 } ) ;
88137} ;
89138
@@ -138,12 +187,13 @@ TuyaDevice.prototype.setStatus = function (on, callback) {
138187/**
139188* Sends a query to the device.
140189* @private
190+ * @param {String } ip - IP of device
141191* @param {Buffer } buffer - buffer of data
142192* @returns {Promise<string> } - returned data
143193*/
144- TuyaDevice . prototype . _send = function ( buffer ) {
194+ TuyaDevice . prototype . _send = function ( ip , buffer ) {
145195 return new Promise ( ( resolve , reject ) => {
146- retryConnect . to ( { port : 6668 , host : this . ip , retryOptions : { retries : 5 } } , ( error , client ) => {
196+ retryConnect . to ( { port : 6668 , host : ip , retryOptions : { retries : 5 } } , ( error , client ) => {
147197 if ( error ) {
148198 reject ( error ) ;
149199 }
@@ -161,19 +211,20 @@ TuyaDevice.prototype._send = function (buffer) {
161211} ;
162212
163213/**
164- * Constructs a protocol-complient buffer given data and command.
214+ * Constructs a protocol-complient buffer given device type, data, and command.
165215* @private
216+ * @param {String } type - type of device
166217* @param {String } data - data to put in buffer
167218* @param {String } command - command (status, on, off, etc.)
168219* @returns {Buffer } buffer - buffer of data
169220*/
170- TuyaDevice . prototype . _constructBuffer = function ( data , command ) {
221+ TuyaDevice . prototype . _constructBuffer = function ( type , data , command ) {
171222 // Construct prefix of packet according to protocol
172- const prefixLength = ( data . toString ( 'hex' ) . length + requests [ this . type ] . suffix . length ) / 2 ;
173- const prefix = requests [ this . type ] . prefix + requests [ this . type ] [ command ] . hexByte + '000000' + prefixLength . toString ( 16 ) ;
223+ const prefixLength = ( data . toString ( 'hex' ) . length + requests [ type ] . suffix . length ) / 2 ;
224+ const prefix = requests [ type ] . prefix + requests [ type ] [ command ] . hexByte + '000000' + prefixLength . toString ( 16 ) ;
174225
175226 // Create final buffer: prefix + data + suffix
176- return Buffer . from ( prefix + data . toString ( 'hex' ) + requests [ this . type ] . suffix , 'hex' ) ;
227+ return Buffer . from ( prefix + data . toString ( 'hex' ) + requests [ type ] . suffix , 'hex' ) ;
177228} ;
178229
179230/**
@@ -203,46 +254,29 @@ TuyaDevice.prototype.getSchema = function () {
203254 } ) ;
204255} ;
205256
206- /**
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-
238257/**
239258* Extracts JSON from a raw buffer and returns it as an object.
240259* @param {Buffer } buffer of data
241260* @returns {Object } extracted object
242261*/
243262TuyaDevice . prototype . _extractJSON = function ( data ) {
244263 data = data . toString ( ) ;
245- data = data . slice ( data . indexOf ( '{' ) , data . lastIndexOf ( '"}' ) + 2 ) ;
264+
265+ // Find the # of occurrences of '{' and make that # match with the # of occurrences of '}'
266+ var leftBrackets = stringOccurrence ( data , '{' ) ;
267+ let occurrences = 0 ;
268+ let currentIndex = 0 ;
269+
270+ while ( occurrences < leftBrackets ) {
271+ let index = data . indexOf ( '}' , currentIndex + 1 ) ;
272+ if ( index != - 1 ) {
273+ currentIndex = index ;
274+ occurrences ++ ;
275+ }
276+ }
277+
278+ data = data . slice ( data . indexOf ( '{' ) , currentIndex + 1 ) ;
279+ console . log ( data )
246280 data = JSON . parse ( data ) ;
247281 return data ;
248282} ;
0 commit comments