homebridge-tuya-plus 3.2.0 → 3.3.0-beta.1

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.
@@ -22,7 +22,11 @@ class TuyaAccessory extends EventEmitter {
22
22
  this.state = {};
23
23
  this._cachedBuffer = Buffer.allocUnsafe(0);
24
24
 
25
- this._msgQueue = async.queue(this[this.context.version < 3.2 ? '_msgHandler_3_1' : this.context.version === '3.4' ? '_msgHandler_3_4' : '_msgHandler_3_3'].bind(this), 1);
25
+ this._msgQueue = async.queue(this[
26
+ this.context.version < 3.2 ? '_msgHandler_3_1' :
27
+ this.context.version === '3.3' ? '_msgHandler_3_3' :
28
+ this.context.version === '3.4' ? '_msgHandler_3_4' : '_msgHandler_3_5'
29
+ ].bind(this), 1);
26
30
 
27
31
  if (this.context.version >= 3.2) {
28
32
  this.context.pingGap = Math.min(this.context.pingGap || 9, 9);
@@ -102,7 +106,7 @@ class TuyaAccessory extends EventEmitter {
102
106
  };
103
107
 
104
108
  this._socket.on('connect', () => {
105
- if (this.context.version !== '3.4') {
109
+ if (this.context.version !== '3.4' && this.context.version !== '3.5') {
106
110
  clearTimeout(this._socket._connTimeout);
107
111
 
108
112
  this.connected = true;
@@ -122,13 +126,13 @@ class TuyaAccessory extends EventEmitter {
122
126
  if (this.context.intro === false) return;
123
127
  this.connected = true;
124
128
 
125
- if (this.context.version === '3.4') {
129
+ if (this.context.version === '3.4' || this.context.version === '3.5') {
126
130
  this._tmpLocalKey = crypto.randomBytes(16);
127
131
  const payload = {
128
132
  data: this._tmpLocalKey,
129
133
  encrypted: true,
130
134
  cmd: 3 //CommandType.BIND
131
- }
135
+ };
132
136
  this._send(payload);
133
137
  } else {
134
138
  this.update();
@@ -139,18 +143,28 @@ class TuyaAccessory extends EventEmitter {
139
143
  this._cachedBuffer = Buffer.concat([this._cachedBuffer, msg]);
140
144
 
141
145
  do {
142
- let startingIndex = this._cachedBuffer.indexOf('000055aa', 'hex');
146
+ let startingIndex;
147
+ if (this.context.version === '3.5') {
148
+ startingIndex = this._cachedBuffer.indexOf('00006699', 'hex');
149
+ } else {
150
+ startingIndex = this._cachedBuffer.indexOf('000055aa', 'hex');
151
+ }
143
152
  if (startingIndex === -1) {
144
153
  this._cachedBuffer = Buffer.allocUnsafe(0);
145
154
  break;
146
155
  }
147
156
  if (startingIndex !== 0) this._cachedBuffer = this._cachedBuffer.slice(startingIndex);
148
157
 
149
- let endingIndex = this._cachedBuffer.indexOf('0000aa55', 'hex');
158
+ let endingIndex;
159
+ if (this.context.version === '3.5') {
160
+ endingIndex = this._cachedBuffer.indexOf('00009966', 'hex');
161
+ if (endingIndex !== -1) endingIndex += 4;
162
+ } else {
163
+ endingIndex = this._cachedBuffer.indexOf('0000aa55', 'hex');
164
+ if (endingIndex !== -1) endingIndex += 4;
165
+ }
150
166
  if (endingIndex === -1) break;
151
167
 
152
- endingIndex += 4;
153
-
154
168
  this._msgQueue.push({msg: this._cachedBuffer.slice(0, endingIndex)});
155
169
 
156
170
  this._cachedBuffer = this._cachedBuffer.slice(endingIndex);
@@ -489,6 +503,125 @@ class TuyaAccessory extends EventEmitter {
489
503
  callback();
490
504
  }
491
505
 
506
+ /*
507
+ * 3.5 Protocol message handler
508
+ */
509
+ _msgHandler_3_5(task, callback) {
510
+ if (!(task.msg instanceof Buffer)) return callback();
511
+
512
+ const len = task.msg.length;
513
+ if (len < 22 ||
514
+ task.msg.readUInt32BE(0) !== 0x00006699 ||
515
+ task.msg.readUInt32BE(len - 4) !== 0x00009966) return callback();
516
+
517
+ const declaredLen = task.msg.readUInt32BE(14);
518
+ if ((len - 22) < declaredLen) return callback();
519
+
520
+ const cmd = task.msg.readUInt32BE(10);
521
+
522
+ if (cmd === 7 || cmd === 13) return callback(); // ignoring
523
+ if (cmd === 9) {
524
+ if (this._socket._pinger) clearTimeout(this._socket._pinger);
525
+ this._socket._pinger = setTimeout(() => {
526
+ this._socket._ping();
527
+ }, (this.context.pingGap || 20) * 1000);
528
+ return callback();
529
+ }
530
+
531
+ const iv = task.msg.slice(18, 30);
532
+ const encrypted = task.msg.slice(30, len - 20); // remove tag + footer
533
+ const tag = task.msg.slice(len - 20, len - 4);
534
+ const aad = task.msg.slice(4, 18); // UUUU + seq + cmd + len (14 bytes)
535
+
536
+ let decrypted;
537
+ try {
538
+ const decipher = crypto.createDecipheriv('aes-128-gcm', this.session_key ?? this.context.key, iv);
539
+ decipher.setAAD(aad);
540
+ decipher.setAuthTag(tag);
541
+ decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
542
+ } catch(ex) {
543
+ this.log.info(`Failed to decrypt message from ${this.context.name} (${this.context.version}) with command ${cmd}:`, ex);
544
+ return callback();
545
+ }
546
+
547
+ // strip return code if present (first 4 bytes)
548
+ let payload = decrypted;
549
+ if (decrypted.length > 4) {
550
+ try {
551
+ JSON.parse(decrypted.toString('utf8'));
552
+ } catch(_) {
553
+ payload = decrypted.slice(4);
554
+ }
555
+ }
556
+
557
+ // remove leading version header
558
+ if (payload.indexOf(this.context.version) === 0) payload = payload.slice(15);
559
+
560
+ // Attempt JSON parse
561
+ let parsedPayload;
562
+ try {
563
+ parsedPayload = JSON.parse(payload.toString('utf8'));
564
+ if (parsedPayload && parsedPayload.data) {
565
+ const tmp = parsedPayload;
566
+ parsedPayload = tmp.data;
567
+ parsedPayload.t = tmp.t;
568
+ }
569
+ } catch(_) {
570
+ parsedPayload = payload; // may be buffer during key exchange
571
+ }
572
+
573
+ // ---- Handle 3‑way key exchange ----
574
+ if (cmd === 4 && Buffer.isBuffer(parsedPayload)) {
575
+ this._tmpRemoteKey = parsedPayload.subarray(0, 16);
576
+ const calcLocalHmac = hmac(this._tmpLocalKey, this.context.key).toString('hex');
577
+ const expLocalHmac = parsedPayload.slice(16, 48).toString('hex');
578
+ if (calcLocalHmac !== expLocalHmac) {
579
+ throw new Error(`HMAC mismatch(keys): expected ${expLocalHmac}, was ${calcLocalHmac}. ${parsedPayload.toString('hex')}`);
580
+ }
581
+
582
+ const payloadToSend = {
583
+ data: hmac(this._tmpRemoteKey, this.context.key),
584
+ encrypted: true,
585
+ cmd: 5 // CommandType.RENAME_DEVICE equivalent
586
+ };
587
+ this._send(payloadToSend);
588
+ clearTimeout(this._socket._connTimeout);
589
+
590
+ // derive session key
591
+ let tmpKey = xorBuffers(this._tmpLocalKey, this._tmpRemoteKey);
592
+ this.session_key = encrypt35(tmpKey, this.context.key, this._tmpLocalKey.slice(0, 12));
593
+
594
+ this.connected = true;
595
+ this.update();
596
+ this.emit('connect');
597
+ if (this._socket._pinger) clearTimeout(this._socket._pinger);
598
+ this._socket._pinger = setTimeout(() => this._socket._ping(), 1000);
599
+ return callback();
600
+ }
601
+
602
+ if (cmd === 10 && parsedPayload === 'json obj data unvalid') {
603
+ this.log.info(`${this.context.name} (${this.context.version}) didn't respond with its current state.`);
604
+ this.emit('change', {}, this.state);
605
+ return callback();
606
+ }
607
+
608
+ switch (cmd) {
609
+ case 8:
610
+ case 10:
611
+ case 16:
612
+ if (parsedPayload && parsedPayload.dps) {
613
+ this._change(parsedPayload.dps);
614
+ } else {
615
+ this.log.info(`Malformed message from ${this.context.name} with command ${cmd}:`, payload.toString('utf8'));
616
+ }
617
+ break;
618
+ default:
619
+ this.log.info(`Odd message from ${this.context.name} with command ${cmd}:`, payload.toString('utf8'));
620
+ }
621
+
622
+ callback();
623
+ }
624
+
492
625
  update(o) {
493
626
  const dps = {};
494
627
  let hasDataPoint = false;
@@ -514,25 +647,25 @@ class TuyaAccessory extends EventEmitter {
514
647
  t,
515
648
  dps
516
649
  };
517
- const data = this.context.version === '3.4'
518
- ? {
519
- data: {
520
- ...payload,
521
- ctype: 0,
522
- t: undefined
523
- },
524
- protocol:5,
525
- t
526
- }
527
- : payload
650
+ const data = (this.context.version === '3.4' || this.context.version === '3.5')
651
+ ? {
652
+ data: {
653
+ ...payload,
654
+ ctype: 0,
655
+ t: undefined
656
+ },
657
+ protocol: 5,
658
+ t
659
+ }
660
+ : payload;
528
661
  result = this._send({
529
662
  data,
530
- cmd: this.context.version === '3.4' ? 13 : 7
663
+ cmd: (this.context.version === '3.4' || this.context.version === '3.5') ? 13 : 7
531
664
  });
532
665
  if (result !== true) this.log.info(" Result", result);
533
666
  if (this.context.sendEmptyUpdate) {
534
667
  //this.log.info(" Sending", this.context.name, 'empty signature');
535
- this._send({cmd: this.context.version === '3.4' ? 13 : 7});
668
+ this._send({cmd: (this.context.version === '3.4' || this.context.version === '3.5') ? 13 : 7});
536
669
  }
537
670
  } else {
538
671
  //this.log.info(`Sending first query to ${this.context.name} (${this.context.version})`);
@@ -541,7 +674,7 @@ class TuyaAccessory extends EventEmitter {
541
674
  gwId: this.context.id,
542
675
  devId: this.context.id
543
676
  },
544
- cmd: this.context.version === '3.4' ? 16 : 10
677
+ cmd: (this.context.version === '3.4' || this.context.version === '3.5') ? 16 : 10
545
678
  });
546
679
  }
547
680
 
@@ -570,7 +703,8 @@ class TuyaAccessory extends EventEmitter {
570
703
 
571
704
  if (this.context.version < 3.2) return this._send_3_1(o);
572
705
  if (this.context.version === '3.3') return this._send_3_3(o);
573
- return this._send_3_4(o);
706
+ if (this.context.version === '3.4') return this._send_3_4(o);
707
+ return this._send_3_5(o);
574
708
  }
575
709
 
576
710
  _send_3_1(o) {
@@ -645,16 +779,6 @@ class TuyaAccessory extends EventEmitter {
645
779
  return this._socket.write(payload);
646
780
  }
647
781
 
648
- _fakeUpdate(dps) {
649
- this.log.info('Fake update:', JSON.stringify(dps));
650
- Object.keys(dps).forEach(dp => {
651
- this.state[dp] = dps[dp];
652
- });
653
- setTimeout(() => {
654
- this.emit('change', dps, this.state);
655
- }, 1000);
656
- }
657
-
658
782
  _send_3_4(o) {
659
783
  let {cmd, data} = {...o};
660
784
 
@@ -713,19 +837,105 @@ class TuyaAccessory extends EventEmitter {
713
837
 
714
838
  return this._socket.write(buffer);
715
839
  }
840
+
841
+ /*
842
+ * 3.5 Protocol send
843
+ */
844
+ _send_3_5(o) {
845
+ let {cmd, data} = {...o};
846
+ // data processing similar to 3.4
847
+ if (!data) data = Buffer.allocUnsafe(0);
848
+ if (!(data instanceof Buffer)) {
849
+ if (typeof data !== 'string') data = JSON.stringify(data);
850
+ data = Buffer.from(data);
851
+ }
852
+
853
+ if (cmd !== 10 && cmd !== 9 && cmd !== 16 && cmd !== 3 && cmd !== 5 && cmd !== 18) {
854
+ const buffer = Buffer.alloc(data.length + 15);
855
+ Buffer.from('3.5').copy(buffer, 0);
856
+ data.copy(buffer, 15);
857
+ data = buffer;
858
+ }
859
+
860
+ const iv = crypto.randomBytes(12);
861
+
862
+ // Increment sequence counter unless empty dp-update
863
+ if ((cmd !== 7 && cmd !== 13) || data.length) this._sendCounter++;
864
+
865
+ const unknownBuf = Buffer.alloc(2); // always 0x0000
866
+ const seqBuf = Buffer.alloc(4);
867
+ seqBuf.writeUInt32BE(this._sendCounter, 0);
868
+ const cmdBuf = Buffer.alloc(4);
869
+ cmdBuf.writeUInt32BE(cmd, 0);
870
+ const lenField = 12 + data.length + 16; // iv + payload + tag
871
+ const lenBuf = Buffer.alloc(4);
872
+ lenBuf.writeUInt32BE(lenField, 0);
873
+
874
+ const aad = Buffer.concat([unknownBuf, seqBuf, cmdBuf, lenBuf]);
875
+
876
+ const key = this.session_key ?? this.context.key;
877
+ const cipher = crypto.createCipheriv('aes-128-gcm', key, iv);
878
+ cipher.setAAD(aad);
879
+ const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
880
+ const tag = cipher.getAuthTag();
881
+
882
+ const prefixBuf = Buffer.from('00006699', 'hex');
883
+ const footerBuf = Buffer.from('00009966', 'hex');
884
+
885
+ const packet = Buffer.concat([
886
+ prefixBuf,
887
+ unknownBuf,
888
+ seqBuf,
889
+ cmdBuf,
890
+ lenBuf,
891
+ iv,
892
+ encrypted,
893
+ tag,
894
+ footerBuf
895
+ ]);
896
+
897
+ return this._socket.write(packet);
898
+ }
899
+
900
+ _fakeUpdate(dps) {
901
+ this.log.info('Fake update:', JSON.stringify(dps));
902
+ Object.keys(dps).forEach(dp => {
903
+ this.state[dp] = dps[dp];
904
+ });
905
+ setTimeout(() => {
906
+ this.emit('change', dps, this.state);
907
+ }, 1000);
908
+ }
716
909
  }
717
910
 
911
+ /* ------------------ Helper utilities ------------------- */
718
912
  const encrypt34 = (data, encryptKey) => {
719
913
  const cipher = crypto.createCipheriv('aes-128-ecb', encryptKey, null);
720
914
  cipher.setAutoPadding(false);
721
915
  let encrypted = cipher.update(data);
722
916
  cipher.final();
723
917
  return encrypted;
724
- }
918
+ };
919
+
920
+ // 3.5 encryption helper (AES‑128‑GCM). Returns ciphertext buffer.
921
+ const encrypt35 = (data, encryptKey, iv) => {
922
+ const cipher = crypto.createCipheriv('aes-128-gcm', encryptKey, iv);
923
+ cipher.setAAD(Buffer.alloc(0));
924
+ const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
925
+ /* auth tag is cipher.getAuthTag() but not required here */
926
+ return encrypted;
927
+ };
725
928
 
726
929
  const hmac = (data, hmacKey) => {
727
- return crypto.createHmac('sha256',hmacKey).update(data, 'utf8').digest();
728
- }
930
+ return crypto.createHmac('sha256', hmacKey).update(data, 'utf8').digest();
931
+ };
932
+
933
+ const xorBuffers = (a, b) => {
934
+ const len = Math.min(a.length, b.length);
935
+ const out = Buffer.alloc(len);
936
+ for (let i = 0; i < len; i++) out[i] = a[i] ^ b[i];
937
+ return out;
938
+ };
729
939
 
730
940
  const crc32LookupTable = [];
731
941
  (() => {
@@ -742,5 +952,4 @@ const getCRC32 = buffer => {
742
952
  return ~crc;
743
953
  };
744
954
 
745
-
746
955
  module.exports = TuyaAccessory;
@@ -4,6 +4,12 @@ const EventEmitter = require('events');
4
4
 
5
5
  const UDP_KEY = Buffer.from('6c1ec8e2bb9bb59ab50b0daf649b410a', 'hex');
6
6
 
7
+ const GCM_DISCOVERY_KEY = crypto.createHash('md5').update('yGAdlopoPVldABfn').digest(); // v3.5 UDP/GCM key
8
+ const PREFIX_55AA = 0x000055aa; // v3.1-3.4
9
+ const PREFIX_6699 = 0x00006699; // v3.5
10
+ const SUFFIX_AA55 = 0x0000aa55;
11
+ const SUFFIX_9966 = 0x00009966;
12
+
7
13
  class TuyaDiscovery extends EventEmitter {
8
14
  constructor() {
9
15
  super();
@@ -30,6 +36,8 @@ class TuyaDiscovery extends EventEmitter {
30
36
  this._running = true;
31
37
  this._start(6666);
32
38
  this._start(6667);
39
+ this._start(7000); // v3.5 direct-reply port
40
+ this._sendV35Probe(); // broadcast a probe so silent 3.5 devices answer us
33
41
 
34
42
  return this;
35
43
  }
@@ -101,42 +109,107 @@ class TuyaDiscovery extends EventEmitter {
101
109
 
102
110
  _onDgramMessage(port, msg, info) {
103
111
  const len = msg.length;
104
- // this.log.info(`Discovery - UDP from ${info.address}:${port} 0x${msg.readUInt32BE(0).toString(16).padStart(8, '0')}...0x${msg.readUInt32BE(len - 4).toString(16).padStart(8, '0')}`);
105
- if (len < 16 ||
106
- msg.readUInt32BE(0) !== 0x000055aa ||
107
- msg.readUInt32BE(len - 4) !== 0x0000aa55
108
- ) {
109
- this.log.error(`Discovery - UDP from ${info.address}:${port}`, msg.toString('hex'));
112
+ const prefix = msg.readUInt32BE(0);
113
+ const suffix = msg.readUInt32BE(len - 4);
114
+
115
+ /* 3.1-3.4 packets: 0x55AA … 0xAA55
116
+ 3.5 packets: 0x6699 … 0x9966 */
117
+ const isV34Frame = prefix === 0x000055aa && suffix === 0x0000aa55;
118
+ const isV35Frame = prefix === 0x00006699 && suffix === 0x00009966;
119
+
120
+ if (!isV34Frame && !isV35Frame) {
121
+ // Not a Tuya discovery frame – ignore.
110
122
  return;
111
123
  }
112
124
 
113
- const size = msg.readUInt32BE(12);
125
+ if (isV34Frame) {
126
+ // original logic v3.1-3.4 devices
127
+ return this._handleV34(msg, port, info);
128
+ }
129
+
130
+ /* v3.5 handling */
131
+ return this._handleV35(msg, port, info);
132
+ }
133
+
134
+ _handleV34(pkt, port, info) {
135
+ const len = pkt.length;
136
+ const size = pkt.readUInt32BE(12);
137
+
114
138
  if (len - size < 8) {
115
139
  this.log.error(`Discovery - UDP from ${info.address}:${port} size ${len - size}`);
116
140
  return;
117
141
  }
118
142
 
119
- //const result = {cmd: msg.readUInt32BE(8)};
120
- const cleanMsg = msg.slice(len - size + 4, len - 8);
143
+ const cleanMsg = pkt.slice(len - size + 4, len - 8);
121
144
 
122
145
  let decryptedMsg;
123
- if (port === 6667) {
146
+ if (port === 6667) { // encrypted replies
124
147
  try {
125
148
  const decipher = crypto.createDecipheriv('aes-128-ecb', UDP_KEY, '');
126
- decryptedMsg = decipher.update(cleanMsg, 'utf8', 'utf8');
127
- decryptedMsg += decipher.final('utf8');
128
- } catch (ex) {}
149
+ decryptedMsg = decipher.update(cleanMsg, 'utf8', 'utf8');
150
+ decryptedMsg += decipher.final('utf8');
151
+ } catch (_) { /* ignore */ }
129
152
  }
130
153
 
131
154
  if (!decryptedMsg) decryptedMsg = cleanMsg.toString('utf8');
132
155
 
133
156
  try {
134
157
  const result = JSON.parse(decryptedMsg);
135
- if (result && result.gwId && result.ip) this._onDiscover(result);
136
- else this.log.error(`Discovery - UDP from ${info.address}:${port} decrypted`, cleanMsg.toString('hex'));
158
+ if (result && result.gwId && result.ip) {
159
+ this._onDiscover(result);
160
+ } else {
161
+ this.log.error(`Discovery - UDP from ${info.address}:${port} decrypted`, cleanMsg.toString('hex'));
162
+ }
137
163
  } catch (ex) {
138
164
  this.log.error(`Discovery - Failed to parse discovery response on port ${port}: ${decryptedMsg}`);
139
- this.log.error(`Discovery - Failed to parse discovery raw message on port ${port}: ${msg.toString('hex')}`);
165
+ this.log.error(`Discovery - Failed to parse discovery raw message on port ${port}: ${pkt.toString('hex')}`);
166
+ }
167
+ }
168
+
169
+
170
+ /*
171
+ * Handle protocol-3.5 discovery replies (0x6699 … 0x9966, AES-GCM).
172
+ *
173
+ * Packet layout (offsets after the 4-byte prefix):
174
+ * 0-3 sequence (uint32)
175
+ * 4-7 command (0 for discovery)
176
+ * 8-11 length (uint32 – bytes that follow up to but NOT incl. suffix)
177
+ * 12-23 IV (12 bytes – GCM nonce)
178
+ * 24-(n-17) cipher (variable)
179
+ * n-16 tag (16-byte GCM auth tag)
180
+ * n-13 … n-1 0x00
181
+ * n n+3 suffix 0x9966
182
+ */
183
+ _handleV35(pkt, srcPort, info) {
184
+ try {
185
+ const len = pkt.length;
186
+ const iv = pkt.slice(16, 28); // 12-byte nonce
187
+ const cipher = pkt.slice(28, len - 20); // ciphertext
188
+ const tag = pkt.slice(len - 20, len - 4); // 16-byte tag
189
+ const aad = pkt.slice(4, 28); // seq+cmd+len+IV
190
+
191
+ const decipher = crypto.createDecipheriv(
192
+ 'aes-128-gcm',
193
+ GCM_DISCOVERY_KEY, // static key defined at top of file
194
+ iv
195
+ );
196
+ decipher.setAuthTag(tag);
197
+ decipher.setAAD(aad);
198
+
199
+ const jsonStr = Buffer.concat([
200
+ decipher.update(cipher),
201
+ decipher.final()
202
+ ]).toString('utf8');
203
+
204
+ const payload = JSON.parse(jsonStr);
205
+ if (payload && payload.gwId && payload.ip) {
206
+ this._onDiscover(payload);
207
+ }
208
+ } catch (ex) {
209
+ this.log.error(
210
+ `Discovery v3.5 – failed to decrypt packet from ${info.address}:${srcPort}:`,
211
+ ex.message
212
+ );
140
213
  }
141
214
  }
142
215
 
@@ -146,6 +219,8 @@ class TuyaDiscovery extends EventEmitter {
146
219
  data.id = data.gwId;
147
220
  delete data.gwId;
148
221
 
222
+ data.version = data.version || '3.5' // Every pre-3.5 device does include version in its broadcast
223
+
149
224
  this.discovered.set(data.id, data.ip);
150
225
 
151
226
  this.emit('discover', data);
@@ -160,6 +235,27 @@ class TuyaDiscovery extends EventEmitter {
160
235
  });
161
236
  }
162
237
  }
238
+
239
+ _sendV35Probe() {
240
+ const socket = dgram.createSocket('udp4');
241
+ const payload = Buffer.from('{"from":"app","ip":"255.255.255.255"}');
242
+ const iv = crypto.randomBytes(12);
243
+ const cipher = crypto.createCipheriv('aes-128-gcm', GCM_DISCOVERY_KEY, iv);
244
+ cipher.setAAD(Buffer.alloc(14)); // UUUU+seq+cmd+len = zeros OK for probe
245
+ const enc = Buffer.concat([cipher.update(payload), cipher.final()]);
246
+ const tag = cipher.getAuthTag();
247
+ const len = Buffer.alloc(4); len.writeUInt32BE(iv.length + enc.length + tag.length, 0);
248
+ const frame = Buffer.concat([
249
+ Buffer.from([0,0,0,0x66,0x99].slice(0,4)), // 6699 prefix
250
+ Buffer.alloc(14), // UUUU + seq + cmd (0) + len placeholder
251
+ len,
252
+ iv,
253
+ enc,
254
+ tag,
255
+ Buffer.from('00009966','hex')
256
+ ]);
257
+ socket.send(frame, 7000, '255.255.255.255', () => socket.close());
258
+ }
163
259
  }
164
260
 
165
261
  module.exports = new TuyaDiscovery();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-tuya-plus",
3
- "version": "3.2.0",
3
+ "version": "3.3.0-beta.1",
4
4
  "description": "A community-maintained Homebridge plugin for controlling Tuya devices locally over LAN. Includes new features, fixes, and updated device support.",
5
5
  "main": "index.js",
6
6
  "scripts": {