@nubisco/homebridge-tuya-local-platform 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +48 -0
  3. package/config.schema.json +852 -0
  4. package/dist/accessories/AirConditioner.accessory.d.ts +50 -0
  5. package/dist/accessories/AirConditioner.accessory.d.ts.map +1 -0
  6. package/dist/accessories/AirConditioner.accessory.js +447 -0
  7. package/dist/accessories/AirConditioner.accessory.js.map +1 -0
  8. package/dist/accessories/AirPurifier.accessory.d.ts +37 -0
  9. package/dist/accessories/AirPurifier.accessory.d.ts.map +1 -0
  10. package/dist/accessories/AirPurifier.accessory.js +414 -0
  11. package/dist/accessories/AirPurifier.accessory.js.map +1 -0
  12. package/dist/accessories/Base.accessory.d.ts +43 -0
  13. package/dist/accessories/Base.accessory.d.ts.map +1 -0
  14. package/dist/accessories/Base.accessory.js +310 -0
  15. package/dist/accessories/Base.accessory.js.map +1 -0
  16. package/dist/accessories/CircuitBreakerMonitor.accessory.d.ts +47 -0
  17. package/dist/accessories/CircuitBreakerMonitor.accessory.d.ts.map +1 -0
  18. package/dist/accessories/CircuitBreakerMonitor.accessory.js +274 -0
  19. package/dist/accessories/CircuitBreakerMonitor.accessory.js.map +1 -0
  20. package/dist/accessories/Convector.accessory.d.ts +43 -0
  21. package/dist/accessories/Convector.accessory.d.ts.map +1 -0
  22. package/dist/accessories/Convector.accessory.js +297 -0
  23. package/dist/accessories/Convector.accessory.js.map +1 -0
  24. package/dist/accessories/CustomMultiOutlet.accessory.d.ts +15 -0
  25. package/dist/accessories/CustomMultiOutlet.accessory.d.ts.map +1 -0
  26. package/dist/accessories/CustomMultiOutlet.accessory.js +105 -0
  27. package/dist/accessories/CustomMultiOutlet.accessory.js.map +1 -0
  28. package/dist/accessories/Dehumidifier.accessory.d.ts +38 -0
  29. package/dist/accessories/Dehumidifier.accessory.d.ts.map +1 -0
  30. package/dist/accessories/Dehumidifier.accessory.js +264 -0
  31. package/dist/accessories/Dehumidifier.accessory.js.map +1 -0
  32. package/dist/accessories/EnergyCharacteristics.d.ts +4 -0
  33. package/dist/accessories/EnergyCharacteristics.d.ts.map +1 -0
  34. package/dist/accessories/EnergyCharacteristics.js +103 -0
  35. package/dist/accessories/EnergyCharacteristics.js.map +1 -0
  36. package/dist/accessories/GarageDoor.accessory.d.ts +30 -0
  37. package/dist/accessories/GarageDoor.accessory.d.ts.map +1 -0
  38. package/dist/accessories/GarageDoor.accessory.js +228 -0
  39. package/dist/accessories/GarageDoor.accessory.js.map +1 -0
  40. package/dist/accessories/MappedHeatPumpHeater.accessory.d.ts +49 -0
  41. package/dist/accessories/MappedHeatPumpHeater.accessory.d.ts.map +1 -0
  42. package/dist/accessories/MappedHeatPumpHeater.accessory.js +292 -0
  43. package/dist/accessories/MappedHeatPumpHeater.accessory.js.map +1 -0
  44. package/dist/accessories/MultiOutlet.accessory.d.ts +15 -0
  45. package/dist/accessories/MultiOutlet.accessory.d.ts.map +1 -0
  46. package/dist/accessories/MultiOutlet.accessory.js +101 -0
  47. package/dist/accessories/MultiOutlet.accessory.js.map +1 -0
  48. package/dist/accessories/OilDiffuser.accessory.d.ts +67 -0
  49. package/dist/accessories/OilDiffuser.accessory.d.ts.map +1 -0
  50. package/dist/accessories/OilDiffuser.accessory.js +466 -0
  51. package/dist/accessories/OilDiffuser.accessory.js.map +1 -0
  52. package/dist/accessories/Outlet.accessory.d.ts +11 -0
  53. package/dist/accessories/Outlet.accessory.d.ts.map +1 -0
  54. package/dist/accessories/Outlet.accessory.js +87 -0
  55. package/dist/accessories/Outlet.accessory.js.map +1 -0
  56. package/dist/accessories/RGBTWLight.accessory.d.ts +37 -0
  57. package/dist/accessories/RGBTWLight.accessory.d.ts.map +1 -0
  58. package/dist/accessories/RGBTWLight.accessory.js +237 -0
  59. package/dist/accessories/RGBTWLight.accessory.js.map +1 -0
  60. package/dist/accessories/RGBTWOutlet.accessory.d.ts +38 -0
  61. package/dist/accessories/RGBTWOutlet.accessory.d.ts.map +1 -0
  62. package/dist/accessories/RGBTWOutlet.accessory.js +305 -0
  63. package/dist/accessories/RGBTWOutlet.accessory.js.map +1 -0
  64. package/dist/accessories/SimpleBlinds.accessory.d.ts +32 -0
  65. package/dist/accessories/SimpleBlinds.accessory.d.ts.map +1 -0
  66. package/dist/accessories/SimpleBlinds.accessory.js +286 -0
  67. package/dist/accessories/SimpleBlinds.accessory.js.map +1 -0
  68. package/dist/accessories/SimpleDimmer.accessory.d.ts +14 -0
  69. package/dist/accessories/SimpleDimmer.accessory.d.ts.map +1 -0
  70. package/dist/accessories/SimpleDimmer.accessory.js +54 -0
  71. package/dist/accessories/SimpleDimmer.accessory.js.map +1 -0
  72. package/dist/accessories/SimpleDimmer2.accessory.d.ts +14 -0
  73. package/dist/accessories/SimpleDimmer2.accessory.d.ts.map +1 -0
  74. package/dist/accessories/SimpleDimmer2.accessory.js +53 -0
  75. package/dist/accessories/SimpleDimmer2.accessory.js.map +1 -0
  76. package/dist/accessories/SimpleFan.accessory.d.ts +21 -0
  77. package/dist/accessories/SimpleFan.accessory.d.ts.map +1 -0
  78. package/dist/accessories/SimpleFan.accessory.js +125 -0
  79. package/dist/accessories/SimpleFan.accessory.js.map +1 -0
  80. package/dist/accessories/SimpleFanLight.accessory.d.ts +31 -0
  81. package/dist/accessories/SimpleFanLight.accessory.d.ts.map +1 -0
  82. package/dist/accessories/SimpleFanLight.accessory.js +191 -0
  83. package/dist/accessories/SimpleFanLight.accessory.js.map +1 -0
  84. package/dist/accessories/SimpleHeater.accessory.d.ts +28 -0
  85. package/dist/accessories/SimpleHeater.accessory.d.ts.map +1 -0
  86. package/dist/accessories/SimpleHeater.accessory.js +175 -0
  87. package/dist/accessories/SimpleHeater.accessory.js.map +1 -0
  88. package/dist/accessories/SimpleLight.accessory.d.ts +11 -0
  89. package/dist/accessories/SimpleLight.accessory.d.ts.map +1 -0
  90. package/dist/accessories/SimpleLight.accessory.js +38 -0
  91. package/dist/accessories/SimpleLight.accessory.js.map +1 -0
  92. package/dist/accessories/Switch.accessory.d.ts +15 -0
  93. package/dist/accessories/Switch.accessory.d.ts.map +1 -0
  94. package/dist/accessories/Switch.accessory.js +101 -0
  95. package/dist/accessories/Switch.accessory.js.map +1 -0
  96. package/dist/accessories/TWLight.accessory.d.ts +19 -0
  97. package/dist/accessories/TWLight.accessory.d.ts.map +1 -0
  98. package/dist/accessories/TWLight.accessory.js +88 -0
  99. package/dist/accessories/TWLight.accessory.js.map +1 -0
  100. package/dist/accessories/Valve.accessory.d.ts +15 -0
  101. package/dist/accessories/Valve.accessory.d.ts.map +1 -0
  102. package/dist/accessories/Valve.accessory.js +144 -0
  103. package/dist/accessories/Valve.accessory.js.map +1 -0
  104. package/dist/accessories/index.d.ts +25 -0
  105. package/dist/accessories/index.d.ts.map +1 -0
  106. package/dist/accessories/index.js +55 -0
  107. package/dist/accessories/index.js.map +1 -0
  108. package/dist/index.d.ts +2 -0
  109. package/dist/index.d.ts.map +1 -0
  110. package/dist/index.js +206 -0
  111. package/dist/index.js.map +1 -0
  112. package/dist/protocol/TuyaAccessory.d.ts +45 -0
  113. package/dist/protocol/TuyaAccessory.d.ts.map +1 -0
  114. package/dist/protocol/TuyaAccessory.js +646 -0
  115. package/dist/protocol/TuyaAccessory.js.map +1 -0
  116. package/dist/protocol/TuyaDiscovery.d.ts +27 -0
  117. package/dist/protocol/TuyaDiscovery.d.ts.map +1 -0
  118. package/dist/protocol/TuyaDiscovery.js +141 -0
  119. package/dist/protocol/TuyaDiscovery.js.map +1 -0
  120. package/dist/types.d.ts +105 -0
  121. package/dist/types.d.ts.map +1 -0
  122. package/dist/types.js +21 -0
  123. package/dist/types.js.map +1 -0
  124. package/package.json +109 -0
@@ -0,0 +1,646 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const net_1 = __importDefault(require("net"));
7
+ const async_1 = __importDefault(require("async"));
8
+ const crypto_1 = __importDefault(require("crypto"));
9
+ const events_1 = require("events");
10
+ const isNonEmptyPlainObject = (o) => {
11
+ if (!o || typeof o !== 'object')
12
+ return false;
13
+ for (const _i in o)
14
+ return true;
15
+ return false;
16
+ };
17
+ class TuyaAccessory extends events_1.EventEmitter {
18
+ log;
19
+ context;
20
+ state = {};
21
+ connected = false;
22
+ _cachedBuffer = Buffer.allocUnsafe(0);
23
+ _msgQueue;
24
+ _socket;
25
+ _connectionAttempts = 0;
26
+ _sendCounter = 0;
27
+ _tmpLocalKey = null;
28
+ _tmpRemoteKey = null;
29
+ session_key = null;
30
+ constructor(props) {
31
+ super();
32
+ if (!(props.id && props.key && props.ip) && !props.fake) {
33
+ if (props.log)
34
+ props.log.info('Insufficient details to initialize:', JSON.stringify(props));
35
+ return;
36
+ }
37
+ this.log = props.log;
38
+ this.context = { version: '3.1', port: 6668, ...props };
39
+ this.state = {};
40
+ this._cachedBuffer = Buffer.allocUnsafe(0);
41
+ const version = parseFloat(this.context.version || '3.1');
42
+ const handlerName = version < 3.2 ? '_msgHandler_3_1' : this.context.version === '3.4' ? '_msgHandler_3_4' : '_msgHandler_3_3';
43
+ this._msgQueue = async_1.default.queue(this[handlerName].bind(this), 1);
44
+ if (version >= 3.2) {
45
+ this.context.pingGap = Math.min(this.context.pingGap || 9, 9);
46
+ }
47
+ this.connected = false;
48
+ if (props.connect !== false)
49
+ this._connect();
50
+ this._connectionAttempts = 0;
51
+ this._sendCounter = 0;
52
+ this._tmpLocalKey = null;
53
+ this._tmpRemoteKey = null;
54
+ this.session_key = null;
55
+ }
56
+ _connect() {
57
+ if (this.context.fake) {
58
+ this.connected = true;
59
+ return void setTimeout(() => {
60
+ this.emit('change', {}, this.state);
61
+ }, 1000);
62
+ }
63
+ this._socket = new net_1.default.Socket();
64
+ this._incrementAttemptCounter();
65
+ (this._socket.reconnect = () => {
66
+ if (this._socket._pinger) {
67
+ clearTimeout(this._socket._pinger);
68
+ this._socket._pinger = null;
69
+ }
70
+ if (this._socket._connTimeout) {
71
+ clearTimeout(this._socket._connTimeout);
72
+ this._socket._connTimeout = null;
73
+ }
74
+ if (this._socket._errorReconnect) {
75
+ clearTimeout(this._socket._errorReconnect);
76
+ this._socket._errorReconnect = null;
77
+ }
78
+ this._socket.setKeepAlive(true);
79
+ this._socket.setNoDelay(true);
80
+ this._socket._connTimeout = setTimeout(() => {
81
+ this._socket.emit('error', new Error('ERR_CONNECTION_TIMED_OUT'));
82
+ }, (this.context.connectTimeout || 30) * 1000);
83
+ this._incrementAttemptCounter();
84
+ this._socket.connect(this.context.port, this.context.ip);
85
+ })();
86
+ this._socket._ping = () => {
87
+ if (this._socket._pinger)
88
+ clearTimeout(this._socket._pinger);
89
+ this._socket._pinger = setTimeout(() => {
90
+ this._socket._pinger = setTimeout(() => {
91
+ this._socket.emit('error', new Error('ERR_PING_TIMED_OUT'));
92
+ }, 5000);
93
+ this._send({ cmd: 9 });
94
+ }, (this.context.pingTimeout || 30) * 1000);
95
+ this._send({ cmd: 9 });
96
+ };
97
+ this._socket.on('connect', () => {
98
+ if (this.context.version !== '3.4') {
99
+ clearTimeout(this._socket._connTimeout);
100
+ this.connected = true;
101
+ this.emit('connect');
102
+ if (this._socket._pinger)
103
+ clearTimeout(this._socket._pinger);
104
+ this._socket._pinger = setTimeout(() => this._socket._ping(), 1000);
105
+ if (this.context.intro === false) {
106
+ this.emit('change', {}, this.state);
107
+ process.nextTick(this.update.bind(this));
108
+ }
109
+ }
110
+ });
111
+ this._socket.on('ready', () => {
112
+ if (this.context.intro === false)
113
+ return;
114
+ this.connected = true;
115
+ if (this.context.version === '3.4') {
116
+ this._tmpLocalKey = crypto_1.default.randomBytes(16);
117
+ const payload = {
118
+ data: this._tmpLocalKey,
119
+ encrypted: true,
120
+ cmd: 3,
121
+ };
122
+ this._send(payload);
123
+ }
124
+ else {
125
+ this.update();
126
+ }
127
+ });
128
+ this._socket.on('data', (msg) => {
129
+ this._cachedBuffer = Buffer.concat([this._cachedBuffer, msg]);
130
+ do {
131
+ const startingIndex = this._cachedBuffer.indexOf('000055aa', 'hex');
132
+ if (startingIndex === -1) {
133
+ this._cachedBuffer = Buffer.allocUnsafe(0);
134
+ break;
135
+ }
136
+ if (startingIndex !== 0)
137
+ this._cachedBuffer = this._cachedBuffer.slice(startingIndex);
138
+ let endingIndex = this._cachedBuffer.indexOf('0000aa55', 'hex');
139
+ if (endingIndex === -1)
140
+ break;
141
+ endingIndex += 4;
142
+ this._msgQueue.push({ msg: this._cachedBuffer.slice(0, endingIndex) });
143
+ this._cachedBuffer = this._cachedBuffer.slice(endingIndex);
144
+ } while (this._cachedBuffer.length);
145
+ });
146
+ this._socket.on('error', (err) => {
147
+ this.connected = false;
148
+ this.log.info(`Socket had a problem and will reconnect to ${this.context.name} (${(err && err.code) || err})`);
149
+ if (err && (err.code === 'ECONNRESET' || err.code === 'EPIPE') && this._connectionAttempts < 10) {
150
+ this.log.debug(`Reconnecting with connection attempts = ${this._connectionAttempts}`);
151
+ return process.nextTick(this._socket.reconnect.bind(this));
152
+ }
153
+ this._socket.destroy();
154
+ let delay = 5000;
155
+ if (err) {
156
+ if (err.code === 'ENOBUFS') {
157
+ this.log.warn('Operating system complained of resource exhaustion; did I open too many sockets?');
158
+ this.log.info('Slowing down retry attempts; if you see this happening often, it could mean some sort of incompatibility.');
159
+ delay = 60000;
160
+ }
161
+ else if (this._connectionAttempts > 10) {
162
+ this.log.info('Slowing down retry attempts; if you see this happening often, it could mean some sort of incompatibility.');
163
+ delay = 60000;
164
+ }
165
+ }
166
+ if (!this._socket._errorReconnect) {
167
+ this.log.debug(`after error setting _connect in ${delay}ms`);
168
+ this._socket._errorReconnect = setTimeout(() => {
169
+ this.log.debug(`executing _connect after ${delay}ms delay`);
170
+ process.nextTick(this._connect.bind(this));
171
+ }, delay);
172
+ }
173
+ });
174
+ this._socket.on('close', () => {
175
+ this.connected = false;
176
+ this.session_key = null;
177
+ });
178
+ this._socket.on('end', () => {
179
+ this.connected = false;
180
+ this.session_key = null;
181
+ this.log.info('Disconnected from', this.context.name);
182
+ });
183
+ }
184
+ _incrementAttemptCounter() {
185
+ this._connectionAttempts++;
186
+ setTimeout(() => {
187
+ this.log.debug(`decrementing this._connectionAttempts, currently ${this._connectionAttempts}`);
188
+ this._connectionAttempts--;
189
+ }, 10000);
190
+ }
191
+ _msgHandler_3_1(task, callback) {
192
+ if (!(task.msg instanceof Buffer))
193
+ return callback();
194
+ const len = task.msg.length;
195
+ if (len < 16 || task.msg.readUInt32BE(0) !== 0x000055aa || task.msg.readUInt32BE(len - 4) !== 0x0000aa55)
196
+ return callback();
197
+ const size = task.msg.readUInt32BE(12);
198
+ if (len - 8 < size)
199
+ return callback();
200
+ const cmd = task.msg.readUInt32BE(8);
201
+ let data = task.msg
202
+ .slice(len - size, len - 8)
203
+ .toString('utf8')
204
+ .trim()
205
+ .replace(/\0/g, '');
206
+ if (this.context.intro === false && cmd !== 9)
207
+ this.log.info('Message from', this.context.name + ':', data);
208
+ switch (cmd) {
209
+ case 7:
210
+ break;
211
+ case 9:
212
+ if (this._socket._pinger)
213
+ clearTimeout(this._socket._pinger);
214
+ this._socket._pinger = setTimeout(() => {
215
+ this._socket._ping();
216
+ }, (this.context.pingGap || 20) * 1000);
217
+ break;
218
+ case 8: {
219
+ let decryptedMsg;
220
+ try {
221
+ const decipher = crypto_1.default.createDecipheriv('aes-128-ecb', this.context.key, '');
222
+ decryptedMsg = decipher.update(data.substr(19), 'base64', 'utf8');
223
+ decryptedMsg += decipher.final('utf8');
224
+ }
225
+ catch (_ex) {
226
+ decryptedMsg = data.substr(19).toString();
227
+ }
228
+ try {
229
+ data = JSON.parse(decryptedMsg);
230
+ }
231
+ catch (_ex) {
232
+ data = decryptedMsg;
233
+ this.log.info(`Odd message from ${this.context.name} with command ${cmd}:`, data);
234
+ this.log.info(`Raw message from ${this.context.name} (${this.context.version}) with command ${cmd}:`, task.msg.toString('hex'));
235
+ break;
236
+ }
237
+ if (data && typeof data === 'object' && data.dps) {
238
+ this._change(data.dps);
239
+ }
240
+ break;
241
+ }
242
+ case 10:
243
+ if (data) {
244
+ if (data === 'json obj data unvalid') {
245
+ this.log.info(`${this.context.name} (${this.context.version}) didn't respond with its current state.`);
246
+ this.emit('change', {}, this.state);
247
+ break;
248
+ }
249
+ try {
250
+ data = JSON.parse(data);
251
+ }
252
+ catch (_ex) {
253
+ this.log.info(`Malformed update from ${this.context.name} with command ${cmd}:`, data);
254
+ this.log.info(`Raw update from ${this.context.name} (${this.context.version}) with command ${cmd}:`, task.msg.toString('hex'));
255
+ break;
256
+ }
257
+ if (data && typeof data === 'object' && data.dps)
258
+ this._change(data.dps);
259
+ }
260
+ break;
261
+ default:
262
+ this.log.info(`Odd message from ${this.context.name} with command ${cmd}:`, data);
263
+ this.log.info(`Raw message from ${this.context.name} (${this.context.version}) with command ${cmd}:`, task.msg.toString('hex'));
264
+ }
265
+ callback();
266
+ }
267
+ _msgHandler_3_3(task, callback) {
268
+ if (!(task.msg instanceof Buffer))
269
+ return callback();
270
+ const len = task.msg.length;
271
+ if (len < 16 || task.msg.readUInt32BE(0) !== 0x000055aa || task.msg.readUInt32BE(len - 4) !== 0x0000aa55)
272
+ return callback();
273
+ const size = task.msg.readUInt32BE(12);
274
+ if (len - 8 < size)
275
+ return callback();
276
+ const cmd = task.msg.readUInt32BE(8);
277
+ if (cmd === 7)
278
+ return callback();
279
+ if (cmd === 9) {
280
+ if (this._socket._pinger)
281
+ clearTimeout(this._socket._pinger);
282
+ this._socket._pinger = setTimeout(() => {
283
+ this._socket._ping();
284
+ }, (this.context.pingGap || 20) * 1000);
285
+ return callback();
286
+ }
287
+ let versionPos = task.msg.indexOf('3.3');
288
+ if (versionPos === -1)
289
+ versionPos = task.msg.indexOf('3.2');
290
+ const cleanMsg = task.msg.slice(versionPos === -1 ? len - size + (task.msg.readUInt32BE(16) & 0xffffff00 ? 0 : 4) : 15 + versionPos, len - 8);
291
+ let decryptedMsg;
292
+ try {
293
+ const decipher = crypto_1.default.createDecipheriv('aes-128-ecb', this.context.key, '');
294
+ decryptedMsg = decipher.update(cleanMsg, undefined, 'utf8');
295
+ decryptedMsg += decipher.final('utf8');
296
+ }
297
+ catch (_ex) {
298
+ decryptedMsg = cleanMsg.toString('utf8');
299
+ }
300
+ if (cmd === 10 && decryptedMsg === 'json obj data unvalid') {
301
+ this.log.info(`${this.context.name} (${this.context.version}) didn't respond with its current state.`);
302
+ this.emit('change', {}, this.state);
303
+ return callback();
304
+ }
305
+ let data;
306
+ try {
307
+ data = JSON.parse(decryptedMsg);
308
+ }
309
+ catch (_ex) {
310
+ this.log.info(`Odd message from ${this.context.name} with command ${cmd}:`, decryptedMsg);
311
+ this.log.info(`Raw message from ${this.context.name} (${this.context.version}) with command ${cmd}:`, task.msg.toString('hex'));
312
+ return callback();
313
+ }
314
+ switch (cmd) {
315
+ case 8:
316
+ case 10:
317
+ if (data) {
318
+ if (data.dps) {
319
+ this._change(data.dps);
320
+ }
321
+ else {
322
+ this.log.info(`Malformed message from ${this.context.name} with command ${cmd}:`, decryptedMsg);
323
+ this.log.info(`Raw message from ${this.context.name} (${this.context.version}) with command ${cmd}:`, task.msg.toString('hex'));
324
+ }
325
+ }
326
+ break;
327
+ default:
328
+ this.log.info(`Odd message from ${this.context.name} with command ${cmd}:`, decryptedMsg);
329
+ this.log.info(`Raw message from ${this.context.name} (${this.context.version}) with command ${cmd}:`, task.msg.toString('hex'));
330
+ }
331
+ callback();
332
+ }
333
+ _msgHandler_3_4(task, callback) {
334
+ if (!(task.msg instanceof Buffer))
335
+ return callback();
336
+ const len = task.msg.length;
337
+ if (len < 16 || task.msg.readUInt32BE(0) !== 0x000055aa || task.msg.readUInt32BE(len - 4) !== 0x0000aa55)
338
+ return callback();
339
+ const size = task.msg.readUInt32BE(12);
340
+ if (len - 8 < size)
341
+ return callback();
342
+ const cmd = task.msg.readUInt32BE(8);
343
+ if (cmd === 7 || cmd === 13)
344
+ return callback();
345
+ if (cmd === 9) {
346
+ if (this._socket._pinger)
347
+ clearTimeout(this._socket._pinger);
348
+ this._socket._pinger = setTimeout(() => {
349
+ this._socket._ping();
350
+ }, (this.context.pingGap || 20) * 1000);
351
+ return callback();
352
+ }
353
+ const versionPos = task.msg.indexOf('3.4');
354
+ const cleanMsg = task.msg.slice(versionPos === -1 ? len - size + (task.msg.readUInt32BE(16) & 0xffffff00 ? 0 : 4) : 15 + versionPos, len - 0x24);
355
+ const expectedCrc = task.msg.slice(len - 0x24, task.msg.length - 4).toString('hex');
356
+ const computedCrc = hmac(task.msg.slice(0, len - 0x24), this.session_key ?? this.context.key).toString('hex');
357
+ if (expectedCrc !== computedCrc) {
358
+ throw new Error(`HMAC mismatch: expected ${expectedCrc}, was ${computedCrc}. ${task.msg.toString('hex')}`);
359
+ }
360
+ const decipher = crypto_1.default.createDecipheriv('aes-128-ecb', this.session_key ?? this.context.key, null);
361
+ decipher.setAutoPadding(false);
362
+ let decryptedMsg = decipher.update(cleanMsg);
363
+ decipher.final();
364
+ decryptedMsg = decryptedMsg.slice(0, decryptedMsg.length - decryptedMsg[decryptedMsg.length - 1]);
365
+ let parsedPayload;
366
+ try {
367
+ let sliced = decryptedMsg;
368
+ if (decryptedMsg.indexOf(this.context.version) === 0) {
369
+ sliced = decryptedMsg.slice(15);
370
+ }
371
+ const res = JSON.parse(sliced.toString());
372
+ if ('data' in res) {
373
+ const resdata = res.data;
374
+ resdata.t = res.t;
375
+ parsedPayload = resdata;
376
+ }
377
+ else {
378
+ parsedPayload = res;
379
+ }
380
+ }
381
+ catch (_) {
382
+ parsedPayload = decryptedMsg;
383
+ }
384
+ if (cmd === 4) {
385
+ this._tmpRemoteKey = parsedPayload.subarray(0, 16);
386
+ const calcLocalHmac = hmac(this._tmpLocalKey, this.session_key ?? this.context.key).toString('hex');
387
+ const expLocalHmac = parsedPayload.slice(16, 16 + 32).toString('hex');
388
+ if (expLocalHmac !== calcLocalHmac) {
389
+ throw new Error(`HMAC mismatch(keys): expected ${expLocalHmac}, was ${calcLocalHmac}. ${parsedPayload.toString('hex')}`);
390
+ }
391
+ const payload = {
392
+ data: hmac(this._tmpRemoteKey, this.context.key),
393
+ encrypted: true,
394
+ cmd: 5,
395
+ };
396
+ this._send(payload);
397
+ clearTimeout(this._socket._connTimeout);
398
+ this.session_key = Buffer.from(this._tmpLocalKey);
399
+ for (let i = 0; i < this._tmpLocalKey.length; i++) {
400
+ this.session_key[i] = this._tmpLocalKey[i] ^ this._tmpRemoteKey[i];
401
+ }
402
+ this.session_key = encrypt34(this.session_key, this.context.key);
403
+ clearTimeout(this._socket._connTimeout);
404
+ this.connected = true;
405
+ this.update();
406
+ this.emit('connect');
407
+ if (this._socket._pinger)
408
+ clearTimeout(this._socket._pinger);
409
+ this._socket._pinger = setTimeout(() => this._socket._ping(), 1000);
410
+ return callback();
411
+ }
412
+ if (cmd === 10 && parsedPayload === 'json obj data unvalid') {
413
+ this.log.info(`${this.context.name} (${this.context.version}) didn't respond with its current state.`);
414
+ this.emit('change', {}, this.state);
415
+ return callback();
416
+ }
417
+ switch (cmd) {
418
+ case 8:
419
+ case 10:
420
+ case 16:
421
+ if (parsedPayload) {
422
+ if (parsedPayload.dps) {
423
+ this._change(parsedPayload.dps);
424
+ }
425
+ else {
426
+ this.log.info(`Malformed message from ${this.context.name} with command ${cmd}:`, decryptedMsg);
427
+ this.log.info(`Raw message from ${this.context.name} (${this.context.version}) with command ${cmd}:`, task.msg.toString('hex'));
428
+ }
429
+ }
430
+ break;
431
+ default:
432
+ this.log.info(`Odd message from ${this.context.name} with command ${cmd}:`, decryptedMsg);
433
+ this.log.info(`Raw message from ${this.context.name} (${this.context.version}) with command ${cmd}:`, task.msg.toString('hex'));
434
+ }
435
+ callback();
436
+ }
437
+ update(o) {
438
+ const dps = {};
439
+ let hasDataPoint = false;
440
+ if (o) {
441
+ Object.keys(o).forEach((key) => {
442
+ if (!isNaN(Number(key))) {
443
+ dps['' + key] = o[key];
444
+ hasDataPoint = true;
445
+ }
446
+ });
447
+ }
448
+ if (this.context.fake) {
449
+ if (hasDataPoint)
450
+ this._fakeUpdate(dps);
451
+ return true;
452
+ }
453
+ let result;
454
+ if (hasDataPoint) {
455
+ const t = (Date.now() / 1000).toFixed(0);
456
+ const payload = {
457
+ devId: this.context.id,
458
+ uid: '',
459
+ t,
460
+ dps,
461
+ };
462
+ const data = this.context.version === '3.4'
463
+ ? {
464
+ data: { ...payload, ctype: 0, t: undefined },
465
+ protocol: 5,
466
+ t,
467
+ }
468
+ : payload;
469
+ result = this._send({
470
+ data: data,
471
+ cmd: this.context.version === '3.4' ? 13 : 7,
472
+ });
473
+ if (result !== true)
474
+ this.log.info(' Result', result);
475
+ if (this.context.sendEmptyUpdate) {
476
+ this._send({ cmd: this.context.version === '3.4' ? 13 : 7 });
477
+ }
478
+ }
479
+ else {
480
+ result = this._send({
481
+ data: {
482
+ gwId: this.context.id,
483
+ devId: this.context.id,
484
+ },
485
+ cmd: this.context.version === '3.4' ? 16 : 10,
486
+ });
487
+ }
488
+ return result;
489
+ }
490
+ _change(data) {
491
+ if (!isNonEmptyPlainObject(data))
492
+ return;
493
+ const changes = {};
494
+ Object.keys(data).forEach((key) => {
495
+ if (data[key] !== this.state[key]) {
496
+ changes[key] = data[key];
497
+ }
498
+ });
499
+ if (isNonEmptyPlainObject(changes)) {
500
+ this.state = { ...this.state, ...data };
501
+ this.emit('change', changes, this.state);
502
+ }
503
+ }
504
+ _send(o) {
505
+ if (this.context.fake)
506
+ return;
507
+ if (!this.connected)
508
+ return false;
509
+ const version = parseFloat(this.context.version || '3.1');
510
+ if (version < 3.2)
511
+ return this._send_3_1(o);
512
+ if (this.context.version === '3.3')
513
+ return this._send_3_3(o);
514
+ return this._send_3_4(o);
515
+ }
516
+ _send_3_1(o) {
517
+ const { cmd, data } = { ...o };
518
+ let msg = '';
519
+ if (data && typeof data !== 'string' && !(data instanceof Buffer)) {
520
+ switch (cmd) {
521
+ case 7: {
522
+ const cipher = crypto_1.default.createCipheriv('aes-128-ecb', this.context.key, '');
523
+ let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'base64');
524
+ encrypted += cipher.final('base64');
525
+ const hash = crypto_1.default
526
+ .createHash('md5')
527
+ .update(`data=${encrypted}||lpv=${this.context.version}||${this.context.key}`, 'utf8')
528
+ .digest('hex')
529
+ .substr(8, 16);
530
+ msg = this.context.version + hash + encrypted;
531
+ break;
532
+ }
533
+ case 10:
534
+ msg = JSON.stringify(data);
535
+ break;
536
+ }
537
+ }
538
+ const payload = Buffer.from(msg);
539
+ const prefix = Buffer.from('000055aa00000000000000' + cmd.toString(16).padStart(2, '0'), 'hex');
540
+ const suffix = Buffer.concat([payload, Buffer.from('000000000000aa55', 'hex')]);
541
+ const len = Buffer.allocUnsafe(4);
542
+ len.writeInt32BE(suffix.length, 0);
543
+ return this._socket.write(Buffer.concat([prefix, len, suffix]));
544
+ }
545
+ _send_3_3(o) {
546
+ const { cmd, data } = { ...o };
547
+ if (cmd !== 7 || data)
548
+ this._sendCounter++;
549
+ const hex = [
550
+ '000055aa',
551
+ this._sendCounter.toString(16).padStart(8, '0'),
552
+ cmd.toString(16).padStart(8, '0'),
553
+ '00000000',
554
+ ];
555
+ if (cmd === 7 && !data)
556
+ hex.push('00000000');
557
+ else if (cmd !== 9 && cmd !== 10)
558
+ hex.push('332e33000000000000000000000000');
559
+ if (data && !(data instanceof Buffer)) {
560
+ const cipher = crypto_1.default.createCipheriv('aes-128-ecb', this.context.key, '');
561
+ let encrypted = cipher.update(Buffer.from(JSON.stringify(data)), undefined, 'hex');
562
+ encrypted += cipher.final('hex');
563
+ hex.push(encrypted);
564
+ }
565
+ hex.push('00000000');
566
+ hex.push('0000aa55');
567
+ const payload = Buffer.from(hex.join(''), 'hex');
568
+ payload.writeUInt32BE(payload.length - 16, 12);
569
+ payload.writeInt32BE(getCRC32(payload.slice(0, payload.length - 8)), payload.length - 8);
570
+ return this._socket.write(payload);
571
+ }
572
+ _fakeUpdate(dps) {
573
+ this.log.info('Fake update:', JSON.stringify(dps));
574
+ Object.keys(dps).forEach((dp) => {
575
+ this.state[dp] = dps[dp];
576
+ });
577
+ setTimeout(() => {
578
+ this.emit('change', dps, this.state);
579
+ }, 1000);
580
+ }
581
+ _send_3_4(o) {
582
+ const { cmd, data: _data } = { ...o };
583
+ let data = _data;
584
+ if (!data) {
585
+ data = Buffer.allocUnsafe(0);
586
+ }
587
+ if (!(data instanceof Buffer)) {
588
+ if (typeof data !== 'string') {
589
+ data = JSON.stringify(data);
590
+ }
591
+ data = Buffer.from(data);
592
+ }
593
+ if (cmd !== 10 && cmd !== 9 && cmd !== 16 && cmd !== 3 && cmd !== 5 && cmd !== 18) {
594
+ const buffer = Buffer.alloc(data.length + 15);
595
+ Buffer.from('3.4').copy(buffer, 0);
596
+ data.copy(buffer, 15);
597
+ data = buffer;
598
+ }
599
+ const padding = 0x10 - (data.length & 0xf);
600
+ const buf34 = Buffer.alloc(data.length + padding, padding);
601
+ data.copy(buf34);
602
+ data = buf34;
603
+ const encrypted = encrypt34(data, this.session_key ?? this.context.key);
604
+ const encryptedBuffer = Buffer.from(encrypted);
605
+ const buffer = Buffer.alloc(encryptedBuffer.length + 52);
606
+ buffer.writeUInt32BE(0x000055aa, 0);
607
+ buffer.writeUInt32BE(cmd, 8);
608
+ buffer.writeUInt32BE(encryptedBuffer.length + 0x24, 12);
609
+ if ((cmd !== 7 && cmd !== 13) || data) {
610
+ this._sendCounter++;
611
+ buffer.writeUInt32BE(this._sendCounter, 4);
612
+ }
613
+ encryptedBuffer.copy(buffer, 16);
614
+ const calculatedCrc = hmac(buffer.slice(0, encryptedBuffer.length + 16), this.session_key ?? this.context.key);
615
+ calculatedCrc.copy(buffer, encryptedBuffer.length + 16);
616
+ buffer.writeUInt32BE(0x0000aa55, encryptedBuffer.length + 48);
617
+ return this._socket.write(buffer);
618
+ }
619
+ }
620
+ const encrypt34 = (data, encryptKey) => {
621
+ const cipher = crypto_1.default.createCipheriv('aes-128-ecb', encryptKey, null);
622
+ cipher.setAutoPadding(false);
623
+ const encrypted = cipher.update(data);
624
+ cipher.final();
625
+ return encrypted;
626
+ };
627
+ const hmac = (data, hmacKey) => {
628
+ return crypto_1.default.createHmac('sha256', hmacKey).update(data).digest();
629
+ };
630
+ const crc32LookupTable = [];
631
+ (() => {
632
+ for (let i = 0; i < 256; i++) {
633
+ let crc = i;
634
+ for (let j = 8; j > 0; j--)
635
+ crc = crc & 1 ? (crc >>> 1) ^ 3988292384 : crc >>> 1;
636
+ crc32LookupTable.push(crc);
637
+ }
638
+ })();
639
+ const getCRC32 = (buffer) => {
640
+ let crc = 0xffffffff;
641
+ for (let i = 0, len = buffer.length; i < len; i++)
642
+ crc = crc32LookupTable[buffer[i] ^ (crc & 0xff)] ^ (crc >>> 8);
643
+ return ~crc;
644
+ };
645
+ exports.default = TuyaAccessory;
646
+ //# sourceMappingURL=TuyaAccessory.js.map