Skip to content

Commit 60b9c85

Browse files
committed
Cleaned up autodiscovery, combined schema() and getStatus() => get()
1 parent a14621c commit 60b9c85

4 files changed

Lines changed: 136 additions & 97 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ See the [docs](docs/API.md).
4949
4. Support arbitrary control schemes for devices as self-reported.
5050
5. Use Promises for all functions?
5151
6. Autodiscovery of devices?
52+
7. Make the JSON parser more reliable.
5253

5354
## Contributors
5455

index.js

Lines changed: 118 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
const dgram = require('dgram');
55
const forge = require('node-forge');
66
const retryConnect = require('net-retry-connect');
7+
const stringOccurrence = require('string-occurrence');
78

89
// Import requests for devices
910
const requests = require('./requests.json');
@@ -22,68 +23,116 @@ const requests = require('./requests.json');
2223
*/
2324
function 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
*/
243262
TuyaDevice.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
};

package-lock.json

Lines changed: 15 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"homepage": "https://github.com/codetheweb/tuyapi#readme",
2727
"dependencies": {
2828
"net-retry-connect": "^0.1.1",
29-
"node-forge": "^0.7.1"
29+
"node-forge": "^0.7.1",
30+
"string-occurrence": "^1.2.0"
3031
},
3132
"devDependencies": {
3233
"documentation": "^5.3.3",

0 commit comments

Comments
 (0)