Skip to content

Commit

Permalink
Fix set() and JSON parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
codetheweb committed Jan 12, 2018
1 parent 60b9c85 commit 5687892
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 133 deletions.
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,11 @@ See the [docs](docs/API.md).

## TODO

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

## Contributors

Expand Down
41 changes: 16 additions & 25 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ Docs
### Table of Contents

- [TuyaDevice](#tuyadevice)
- [getStatus](#getstatus)
- [setStatus](#setstatus)
- [getSchema](#getschema)
- [discoverDevices](#discoverdevices)
- [resolveIds](#resolveids)
- [get](#get)
- [set](#set)
- [\_extractJSON](#_extractjson)

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

### getStatus
### resolveIds

Gets the device's current status.
Resolves IDs stored in class to IPs.

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

### get

Gets the device's current status. Defaults to returning only the first 'dps', but by setting {schema: true} you can get everything.

**Parameters**

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

### setStatus
### set

Sets the device's status.

**Parameters**

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

### getSchema

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

### discoverDevices

Attempts to autodiscover devices (i.e. translate device ID to IP).

**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.
Expand Down
198 changes: 101 additions & 97 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,28 @@ const requests = require('./requests.json');
function TuyaDevice(options) {
this.devices = [];

// If argument is [{id: '', key: ''}]
if (options.constructor === Array) {
if (options.constructor === Array) { // If argument is [{id: '', key: ''}]
this.devices = options;
}

// If argument is {id: '', key: ''}
else if (options.constructor === Object) {
} else if (options.constructor === Object) { // If argument is {id: '', key: ''}
this.devices = [options];
}

// standardize devices array
for (var i = 0; i < this.devices.length; i++) {
if (this.devices[i].type === undefined) { this.devices[i].type = 'outlet'; }
if (this.devices[i].port === undefined) { this.devices[i].port = 6668; }
if (this.devices[i].version === undefined) { this.devices[i].version = 3.1; }
// Standardize devices array
for (let i = 0; i < this.devices.length; i++) {
if (this.devices[i].type === undefined) {
this.devices[i].type = 'outlet';
}
if (this.devices[i].uid === undefined) {
this.devices[i].uid = '';
}
if (this.devices[i].port === undefined) {
this.devices[i].port = 6668;
}
if (this.devices[i].version === undefined) {
this.devices[i].version = 3.1;
}

// create cipher from key
// Create cipher from key
this.devices[i].cipher = forge.cipher.createCipher('AES-ECB', this.devices[i].key);
}
}
Expand All @@ -49,43 +54,44 @@ function TuyaDevice(options) {
* Resolves IDs stored in class to IPs.
* @returns {Promise<Boolean>} - true if IPs were found and devices are ready to be used
*/
TuyaDevice.prototype.resolveIds = function() {
// Create new listener if it hasn't already been created
if (this.listener == undefined) {
this.listener = dgram.createSocket('udp4');
this.listener.bind(6666);
}

// find devices that need an IP
var needIP = [];
for (var i = 0; i < this.devices.length; i++) {
if (this.devices[i].ip == undefined) {
TuyaDevice.prototype.resolveIds = function () {
// Create new listener
this.listener = dgram.createSocket('udp4');
this.listener.bind(6666);

// Find devices that need an IP
const needIP = [];
for (let i = 0; i < this.devices.length; i++) {
if (this.devices[i].ip === undefined) {
needIP.push(this.devices[i].id);
}
}

// todo: add timeout for when IP cannot be found, then reject(with error)
// Todo: add timeout for when IP cannot be found, then reject(with error)
// add IPs to devices in array and return true
return new Promise((resolve, reject) => {
this.listener.on('message', (message, info) => {
let thisId = this._extractJSON(message).gwId;
return new Promise(resolve => {
this.listener.on('message', message => {
const thisId = this._extractJSON(message).gwId;

if (needIP.length > 0) {
if (needIP.includes(thisId)) {
var deviceIndex = this.devices.findIndex(device => {
if (device.id === thisId) { return true; }
const deviceIndex = this.devices.findIndex(device => {
if (device.id === thisId) {
return true;
}
return false;
});

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

needIP.splice(needIP.indexOf(thisId), 1);
}
}
else { // all devices have been resolved
} else { // All devices have been resolved
this.listener.close();
this.listener.removeAllListeners();
resolve(true);
}
})
});
});
};

Expand All @@ -95,18 +101,20 @@ TuyaDevice.prototype.resolveIds = function() {
* @param {function(error, result)} callback
*/
TuyaDevice.prototype.get = function (options) {
var currentDevice;
let currentDevice;

// If no ID is provided
if (options === undefined || options.id === undefined) {
currentDevice = this.devices[0]; // use first device in array
}
else { // otherwise
currentDevice = this.devices[0]; // Use first device in array
} else { // Otherwise
// find the device by id in this.devices
let index = this.devices.findIndex(device => {
if (device.id === options.id) { return true; }
const index = this.devices.findIndex(device => {
if (device.id === options.id) {
return true;
}
return false;
});
currentDevice = this.devices[index]
currentDevice = this.devices[index];
}

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

return new Promise((resolve, reject) => {
return new Promise(resolve => {
this._send(currentDevice.ip, buffer).then(data => {
// Extract returned JSON
data = this._extractJSON(data);

if (options != undefined && options.schema == true) {
if (options !== undefined && options.schema === true) {
resolve(data);
}
else {
resolve(data.dps['1'])
} else {
resolve(data.dps['1']);
}
});
});
Expand All @@ -139,48 +146,73 @@ TuyaDevice.prototype.get = function (options) {
/**
* Sets the device's status.
* @param {boolean} on - `true` for on, `false` for off
* {id, set: true|false, dps:1}
* @param {function(error, result)} callback - returns `true` if the command succeeded
*/
TuyaDevice.prototype.setStatus = function (on, callback) {
const thisRequest = requests[this.type][on ? 'on' : 'off'];
TuyaDevice.prototype.set = function (options) {
let currentDevice;

// If no ID is provided
if (options === undefined || options.id === undefined) {
currentDevice = this.devices[0]; // Use first device in array
} else { // Otherwise
// find the device by id in this.devices
const index = this.devices.findIndex(device => {
if (device.id === options.id) {
return true;
}
return false;
});
currentDevice = this.devices[index];
}

const thisRequest = requests[currentDevice.type].set.command;

// Add data to command
const now = new Date();
if ('gwId' in thisRequest.command) {
thisRequest.command.gwId = this.id;
if ('gwId' in thisRequest) {
thisRequest.gwId = currentDevice.id;
}
if ('devId' in thisRequest) {
thisRequest.devId = currentDevice.id;
}
if ('devId' in thisRequest.command) {
thisRequest.command.devId = this.id;
if ('uid' in thisRequest) {
thisRequest.uid = currentDevice.uid;
}
if ('uid' in thisRequest.command) {
thisRequest.command.uid = this.uid;
if ('t' in thisRequest) {
thisRequest.t = (parseInt(now.getTime() / 1000, 10)).toString();
}
if ('t' in thisRequest.command) {
thisRequest.command.t = (parseInt(now.getTime() / 1000, 10)).toString();

if (options.dps === undefined) {
thisRequest.dps = {1: options.set};
} else {
thisRequest.dps[options.dps.toString] = options.set;
}

// Encrypt data
this.cipher.start({iv: ''});
this.cipher.update(forge.util.createBuffer(JSON.stringify(thisRequest.command), 'utf8'));
this.cipher.finish();
currentDevice.cipher.start({iv: ''});
currentDevice.cipher.update(forge.util.createBuffer(JSON.stringify(thisRequest), 'utf8'));
currentDevice.cipher.finish();

// Encode binary data to Base64
const data = forge.util.encode64(this.cipher.output.data);
const data = forge.util.encode64(currentDevice.cipher.output.data);

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

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

// Send request to change status
this._send(buffer).then(data => {
return callback(null, true);
}).catch(err => {
return callback(err, null);
return new Promise((resolve, reject) => {
this._send(currentDevice.ip, buffer).then(() => {
resolve(true);
}).catch(err => {
reject(err);
});
});
};

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

/**
* Gets control schema from device.
* @returns {Promise<Object>} schema - object of parsed JSON
*/
TuyaDevice.prototype.getSchema = function () {
// Create byte buffer from hex data
const thisData = Buffer.from(JSON.stringify({
gwId: this.id,
devId: this.id
}));
const buffer = this._constructBuffer(thisData, 'status');

return new Promise((resolve, reject) => {
this._send(buffer).then(data => {
// Extract returned JSON
try {
data = data.toString();
data = data.slice(data.indexOf('{'), data.lastIndexOf('}') + 1);
data = JSON.parse(data);
return resolve(data.dps);
} catch (err) {
return reject(err);
}
});
});
};

/**
* Extracts JSON from a raw buffer and returns it as an object.
* @param {Buffer} buffer of data
Expand All @@ -263,20 +268,19 @@ TuyaDevice.prototype._extractJSON = function (data) {
data = data.toString();

// Find the # of occurrences of '{' and make that # match with the # of occurrences of '}'
var leftBrackets = stringOccurrence(data, '{');
const leftBrackets = stringOccurrence(data, '{');
let occurrences = 0;
let currentIndex = 0;

while (occurrences < leftBrackets) {
let index = data.indexOf('}', currentIndex + 1);
if (index != -1) {
const index = data.indexOf('}', currentIndex + 1);
if (index !== -1) {
currentIndex = index;
occurrences ++;
occurrences++;
}
}

data = data.slice(data.indexOf('{'), currentIndex + 1);
console.log(data)
data = JSON.parse(data);
return data;
};
Expand Down
Loading

0 comments on commit 5687892

Please sign in to comment.