Skip to content

Commit 5687892

Browse files
committed
Fix set() and JSON parsing
1 parent 60b9c85 commit 5687892

File tree

5 files changed

+122
-133
lines changed

5 files changed

+122
-133
lines changed

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,11 @@ See the [docs](docs/API.md).
4343

4444
## TODO
4545

46-
1. ~~Reuse a TCP connection between subsequent commands, instead of creating a new one every time.~~
47-
2. ~~Figure out what the hex-encoded 'padding' is.~~
4846
3. Better documentation.
4947
4. Support arbitrary control schemes for devices as self-reported.
5048
5. Use Promises for all functions?
51-
6. Autodiscovery of devices?
52-
7. Make the JSON parser more reliable.
49+
7. Add automated tests
50+
8. Document details of protocol
5351

5452
## Contributors
5553

docs/API.md

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@ Docs
55
### Table of Contents
66

77
- [TuyaDevice](#tuyadevice)
8-
- [getStatus](#getstatus)
9-
- [setStatus](#setstatus)
10-
- [getSchema](#getschema)
11-
- [discoverDevices](#discoverdevices)
8+
- [resolveIds](#resolveids)
9+
- [get](#get)
10+
- [set](#set)
1211
- [\_extractJSON](#_extractjson)
1312

1413
## TuyaDevice
@@ -26,41 +25,33 @@ Represents a Tuya device.
2625
- `options.key` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** encryption key of device
2726
- `options.version` **[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** protocol version (optional, default `3.1`)
2827

29-
### getStatus
28+
### resolveIds
3029

31-
Gets the device's current status.
30+
Resolves IDs stored in class to IPs.
31+
32+
Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)>** true if IPs were found and devices are ready to be used
33+
34+
### get
35+
36+
Gets the device's current status. Defaults to returning only the first 'dps', but by setting {schema: true} you can get everything.
3237

3338
**Parameters**
3439

40+
- `options`
41+
- `ID` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** optional, ID of device. Defaults to first device.
3542
- `callback` **function ([error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error), result)**
3643

37-
### setStatus
44+
### set
3845

3946
Sets the device's status.
4047

4148
**Parameters**
4249

50+
- `options`
4351
- `on` **[boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** `true` for on, `false` for off
52+
{id, set: true|false, dps:1}
4453
- `callback` **function ([error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error), result)** returns `true` if the command succeeded
4554

46-
### getSchema
47-
48-
Gets control schema from device.
49-
50-
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
51-
52-
### discoverDevices
53-
54-
Attempts to autodiscover devices (i.e. translate device ID to IP).
55-
56-
**Parameters**
57-
58-
- `ids`
59-
- `callback`
60-
- `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
61-
62-
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
63-
6455
### \_extractJSON
6556

6657
Extracts JSON from a raw buffer and returns it as an object.

index.js

Lines changed: 101 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,28 @@ const requests = require('./requests.json');
2424
function TuyaDevice(options) {
2525
this.devices = [];
2626

27-
// If argument is [{id: '', key: ''}]
28-
if (options.constructor === Array) {
27+
if (options.constructor === Array) { // If argument is [{id: '', key: ''}]
2928
this.devices = options;
30-
}
31-
32-
// If argument is {id: '', key: ''}
33-
else if (options.constructor === Object) {
29+
} else if (options.constructor === Object) { // If argument is {id: '', key: ''}
3430
this.devices = [options];
3531
}
3632

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; }
33+
// Standardize devices array
34+
for (let i = 0; i < this.devices.length; i++) {
35+
if (this.devices[i].type === undefined) {
36+
this.devices[i].type = 'outlet';
37+
}
38+
if (this.devices[i].uid === undefined) {
39+
this.devices[i].uid = '';
40+
}
41+
if (this.devices[i].port === undefined) {
42+
this.devices[i].port = 6668;
43+
}
44+
if (this.devices[i].version === undefined) {
45+
this.devices[i].version = 3.1;
46+
}
4247

43-
// create cipher from key
48+
// Create cipher from key
4449
this.devices[i].cipher = forge.cipher.createCipher('AES-ECB', this.devices[i].key);
4550
}
4651
}
@@ -49,43 +54,44 @@ function TuyaDevice(options) {
4954
* Resolves IDs stored in class to IPs.
5055
* @returns {Promise<Boolean>} - true if IPs were found and devices are ready to be used
5156
*/
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) {
57+
TuyaDevice.prototype.resolveIds = function () {
58+
// Create new listener
59+
this.listener = dgram.createSocket('udp4');
60+
this.listener.bind(6666);
61+
62+
// Find devices that need an IP
63+
const needIP = [];
64+
for (let i = 0; i < this.devices.length; i++) {
65+
if (this.devices[i].ip === undefined) {
6366
needIP.push(this.devices[i].id);
6467
}
6568
}
6669

67-
// todo: add timeout for when IP cannot be found, then reject(with error)
70+
// Todo: add timeout for when IP cannot be found, then reject(with error)
6871
// 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+
return new Promise(resolve => {
73+
this.listener.on('message', message => {
74+
const thisId = this._extractJSON(message).gwId;
7275

7376
if (needIP.length > 0) {
7477
if (needIP.includes(thisId)) {
75-
var deviceIndex = this.devices.findIndex(device => {
76-
if (device.id === thisId) { return true; }
78+
const deviceIndex = this.devices.findIndex(device => {
79+
if (device.id === thisId) {
80+
return true;
81+
}
82+
return false;
7783
});
7884

7985
this.devices[deviceIndex].ip = this._extractJSON(message).ip;
8086

8187
needIP.splice(needIP.indexOf(thisId), 1);
8288
}
83-
}
84-
else { // all devices have been resolved
89+
} else { // All devices have been resolved
90+
this.listener.close();
8591
this.listener.removeAllListeners();
8692
resolve(true);
8793
}
88-
})
94+
});
8995
});
9096
};
9197

@@ -95,18 +101,20 @@ TuyaDevice.prototype.resolveIds = function() {
95101
* @param {function(error, result)} callback
96102
*/
97103
TuyaDevice.prototype.get = function (options) {
98-
var currentDevice;
104+
let currentDevice;
99105

100106
// If no ID is provided
101107
if (options === undefined || options.id === undefined) {
102-
currentDevice = this.devices[0]; // use first device in array
103-
}
104-
else { // otherwise
108+
currentDevice = this.devices[0]; // Use first device in array
109+
} else { // Otherwise
105110
// find the device by id in this.devices
106-
let index = this.devices.findIndex(device => {
107-
if (device.id === options.id) { return true; }
111+
const index = this.devices.findIndex(device => {
112+
if (device.id === options.id) {
113+
return true;
114+
}
115+
return false;
108116
});
109-
currentDevice = this.devices[index]
117+
currentDevice = this.devices[index];
110118
}
111119

112120
// Add data to command
@@ -121,16 +129,15 @@ TuyaDevice.prototype.get = function (options) {
121129
const thisData = Buffer.from(JSON.stringify(requests[currentDevice.type].status.command));
122130
const buffer = this._constructBuffer(currentDevice.type, thisData, 'status');
123131

124-
return new Promise((resolve, reject) => {
132+
return new Promise(resolve => {
125133
this._send(currentDevice.ip, buffer).then(data => {
126134
// Extract returned JSON
127135
data = this._extractJSON(data);
128136

129-
if (options != undefined && options.schema == true) {
137+
if (options !== undefined && options.schema === true) {
130138
resolve(data);
131-
}
132-
else {
133-
resolve(data.dps['1'])
139+
} else {
140+
resolve(data.dps['1']);
134141
}
135142
});
136143
});
@@ -139,48 +146,73 @@ TuyaDevice.prototype.get = function (options) {
139146
/**
140147
* Sets the device's status.
141148
* @param {boolean} on - `true` for on, `false` for off
149+
* {id, set: true|false, dps:1}
142150
* @param {function(error, result)} callback - returns `true` if the command succeeded
143151
*/
144-
TuyaDevice.prototype.setStatus = function (on, callback) {
145-
const thisRequest = requests[this.type][on ? 'on' : 'off'];
152+
TuyaDevice.prototype.set = function (options) {
153+
let currentDevice;
154+
155+
// If no ID is provided
156+
if (options === undefined || options.id === undefined) {
157+
currentDevice = this.devices[0]; // Use first device in array
158+
} else { // Otherwise
159+
// find the device by id in this.devices
160+
const index = this.devices.findIndex(device => {
161+
if (device.id === options.id) {
162+
return true;
163+
}
164+
return false;
165+
});
166+
currentDevice = this.devices[index];
167+
}
168+
169+
const thisRequest = requests[currentDevice.type].set.command;
146170

147171
// Add data to command
148172
const now = new Date();
149-
if ('gwId' in thisRequest.command) {
150-
thisRequest.command.gwId = this.id;
173+
if ('gwId' in thisRequest) {
174+
thisRequest.gwId = currentDevice.id;
175+
}
176+
if ('devId' in thisRequest) {
177+
thisRequest.devId = currentDevice.id;
151178
}
152-
if ('devId' in thisRequest.command) {
153-
thisRequest.command.devId = this.id;
179+
if ('uid' in thisRequest) {
180+
thisRequest.uid = currentDevice.uid;
154181
}
155-
if ('uid' in thisRequest.command) {
156-
thisRequest.command.uid = this.uid;
182+
if ('t' in thisRequest) {
183+
thisRequest.t = (parseInt(now.getTime() / 1000, 10)).toString();
157184
}
158-
if ('t' in thisRequest.command) {
159-
thisRequest.command.t = (parseInt(now.getTime() / 1000, 10)).toString();
185+
186+
if (options.dps === undefined) {
187+
thisRequest.dps = {1: options.set};
188+
} else {
189+
thisRequest.dps[options.dps.toString] = options.set;
160190
}
161191

162192
// Encrypt data
163-
this.cipher.start({iv: ''});
164-
this.cipher.update(forge.util.createBuffer(JSON.stringify(thisRequest.command), 'utf8'));
165-
this.cipher.finish();
193+
currentDevice.cipher.start({iv: ''});
194+
currentDevice.cipher.update(forge.util.createBuffer(JSON.stringify(thisRequest), 'utf8'));
195+
currentDevice.cipher.finish();
166196

167197
// Encode binary data to Base64
168-
const data = forge.util.encode64(this.cipher.output.data);
198+
const data = forge.util.encode64(currentDevice.cipher.output.data);
169199

170200
// Create MD5 signature
171-
const preMd5String = 'data=' + data + '||lpv=' + this.version + '||' + this.key;
201+
const preMd5String = 'data=' + data + '||lpv=' + currentDevice.version + '||' + currentDevice.key;
172202
const md5hash = forge.md.md5.create().update(preMd5String).digest().toHex();
173203
const md5 = md5hash.toString().toLowerCase().substr(8, 16);
174204

175205
// Create byte buffer from hex data
176-
const thisData = Buffer.from(this.version + md5 + data);
177-
const buffer = this._constructBuffer(thisData, [on ? 'on' : 'off']);
206+
const thisData = Buffer.from(currentDevice.version + md5 + data);
207+
const buffer = this._constructBuffer(currentDevice.type, thisData, 'set');
178208

179209
// Send request to change status
180-
this._send(buffer).then(data => {
181-
return callback(null, true);
182-
}).catch(err => {
183-
return callback(err, null);
210+
return new Promise((resolve, reject) => {
211+
this._send(currentDevice.ip, buffer).then(() => {
212+
resolve(true);
213+
}).catch(err => {
214+
reject(err);
215+
});
184216
});
185217
};
186218

@@ -227,33 +259,6 @@ TuyaDevice.prototype._constructBuffer = function (type, data, command) {
227259
return Buffer.from(prefix + data.toString('hex') + requests[type].suffix, 'hex');
228260
};
229261

230-
/**
231-
* Gets control schema from device.
232-
* @returns {Promise<Object>} schema - object of parsed JSON
233-
*/
234-
TuyaDevice.prototype.getSchema = function () {
235-
// Create byte buffer from hex data
236-
const thisData = Buffer.from(JSON.stringify({
237-
gwId: this.id,
238-
devId: this.id
239-
}));
240-
const buffer = this._constructBuffer(thisData, 'status');
241-
242-
return new Promise((resolve, reject) => {
243-
this._send(buffer).then(data => {
244-
// Extract returned JSON
245-
try {
246-
data = data.toString();
247-
data = data.slice(data.indexOf('{'), data.lastIndexOf('}') + 1);
248-
data = JSON.parse(data);
249-
return resolve(data.dps);
250-
} catch (err) {
251-
return reject(err);
252-
}
253-
});
254-
});
255-
};
256-
257262
/**
258263
* Extracts JSON from a raw buffer and returns it as an object.
259264
* @param {Buffer} buffer of data
@@ -263,20 +268,19 @@ TuyaDevice.prototype._extractJSON = function (data) {
263268
data = data.toString();
264269

265270
// Find the # of occurrences of '{' and make that # match with the # of occurrences of '}'
266-
var leftBrackets = stringOccurrence(data, '{');
271+
const leftBrackets = stringOccurrence(data, '{');
267272
let occurrences = 0;
268273
let currentIndex = 0;
269274

270275
while (occurrences < leftBrackets) {
271-
let index = data.indexOf('}', currentIndex + 1);
272-
if (index != -1) {
276+
const index = data.indexOf('}', currentIndex + 1);
277+
if (index !== -1) {
273278
currentIndex = index;
274-
occurrences ++;
279+
occurrences++;
275280
}
276281
}
277282

278283
data = data.slice(data.indexOf('{'), currentIndex + 1);
279-
console.log(data)
280284
data = JSON.parse(data);
281285
return data;
282286
};

0 commit comments

Comments
 (0)