forked from codetheweb/tuyapi
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
165 lines (143 loc) · 4.97 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
'use strict';
// Import packages
const forge = require('node-forge');
const recon = require('@codetheweb/recon');
const waitUntil = require('wait-until');
// Import requests for devices
const requests = require('./requests.json');
/**
* Represents a Tuya device.
* @constructor
* @param {Object} options - options for constructing a TuyaDevice
* @param {string} [options.type='outlet'] - type of device
* @param {string} options.ip - IP of device
* @param {number} [options.port=6668] - port of device
* @param {string} options.id - ID of device
* @param {string} options.uid - UID of device
* @param {string} options.key - encryption key of device
* @param {number} [options.version=3.1] - protocol version
*/
function TuyaDevice(options) {
// Init properties
this.type = options.type || 'outlet';
this.ip = options.ip;
this.port = options.port || 6668;
this.id = options.id;
this.uid = options.uid;
this.key = options.key;
this.version = options.version || 3.1;
// Create cipher object
this.cipher = forge.cipher.createCipher('AES-ECB', this.key);
// Create connection
// this.client = new connect({host: this.ip, port: this.port});
this.client = recon(this.ip, this.port, {retryErrors: ['ECONNREFUSED', 'ECONNRESET']});
}
/**
* Gets the device's current status.
* @param {function(error, result)} callback
*/
TuyaDevice.prototype.getStatus = function (callback) {
// Add data to command
if ('gwId' in requests[this.type].status.command) {
requests[this.type].status.command.gwId = this.id;
}
if ('devId' in requests[this.type].status.command) {
requests[this.type].status.command.devId = this.id;
}
// Create byte buffer from hex data
const thisData = Buffer.from(JSON.stringify(requests[this.type].status.command));
// Create data prefix
const prefixSum = (thisData.toString('hex').length + requests[this.type].status.suffix.length) / 2;
const commandType = '0a';
const prefix = '000055aa0000006c000000' + commandType + '000000' + prefixSum.toString(16);
const buffer = Buffer.from(prefix + thisData.toString('hex') + requests[this.type].status.suffix, 'hex');
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 callback(null, data.dps['1']);
} catch (err) {
return callback(err, null);
}
});
};
/**
* Sets the device's status.
* @param {boolean} on - `true` for on, `false` for off
* @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'];
// Add data to command
const now = new Date();
if ('gwId' in thisRequest.command) {
thisRequest.command.gwId = this.id;
}
if ('devId' in thisRequest.command) {
thisRequest.command.devId = this.id;
}
if ('uid' in thisRequest.command) {
thisRequest.command.uid = this.uid;
}
if ('t' in thisRequest.command) {
thisRequest.command.t = (parseInt(now.getTime() / 1000, 10)).toString();
}
// Encrypt data
this.cipher.start({iv: ''});
this.cipher.update(forge.util.createBuffer(JSON.stringify(thisRequest.command), 'utf8'));
this.cipher.finish();
// Encode binary data to Base64
const data = forge.util.encode64(this.cipher.output.data);
// Create MD5 signature
const preMd5String = 'data=' + data + '||lpv=' + this.version + '||' + this.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);
// Create data prefix
const prefixSum = (thisData.toString('hex').length + requests[this.type].status.suffix.length) / 2;
const commandType = '07';
const prefix = '000055aa0000006c000000' + commandType + '000000' + prefixSum.toString(16);
const buffer = Buffer.from(prefix + thisData.toString('hex') + thisRequest.suffix, 'hex');
// Send request to change status
this._send(buffer).then(data => {
return callback(null, true);
}).catch(err => {
return callback(err, null);
});
};
/**
* Sends a query to the device.
* @private
* @param {Buffer} buffer - buffer of data
* @returns {Promise<string>} - returned data
*/
TuyaDevice.prototype._send = function (buffer) {
const me = this;
return new Promise((resolve, reject) => {
// Wait for device to become available
waitUntil(500, 40, () => {
return me.client.writable;
}, result => {
if (result === false) {
return reject(new Error('timeout'));
}
me.client.write(buffer);
me.client.on('data', data => {
return resolve(data);
});
});
});
};
/**
* Breaks connection to device and destroys socket.
* @returns {True}
*/
TuyaDevice.prototype.destroy = function () {
this.client.end();
this.client.destroy();
return true;
};
module.exports = TuyaDevice;