homebridge-tuya-plus 3.14.0-dev.42 → 3.14.0-dev.44
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.
- package/lib/TuyaCloudApi.js +6 -1
- package/lib/TuyaCloudDevice.js +28 -3
- package/package.json +1 -1
- package/test/TuyaCloudDevice.test.js +36 -0
package/lib/TuyaCloudApi.js
CHANGED
|
@@ -87,6 +87,10 @@ class TuyaCloudApi {
|
|
|
87
87
|
this.uid = null; // linked app-account uid (needed for realtime MQTT)
|
|
88
88
|
this._tokenExpiry = 0; // ms epoch when the token must be refreshed
|
|
89
89
|
this._tokenPromise = null; // in-flight token request (de-dupes concurrent callers)
|
|
90
|
+
|
|
91
|
+
// Reuse TLS connections across requests instead of a fresh handshake each
|
|
92
|
+
// time — noticeably cuts per-command latency and the startup read burst.
|
|
93
|
+
this._agent = new https.Agent({keepAlive: true, maxSockets: 8});
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
isConfigured() {
|
|
@@ -365,7 +369,8 @@ class TuyaCloudApi {
|
|
|
365
369
|
port: url.port || 443,
|
|
366
370
|
path: url.pathname + url.search,
|
|
367
371
|
method,
|
|
368
|
-
headers
|
|
372
|
+
headers,
|
|
373
|
+
agent: this._agent
|
|
369
374
|
}, resp => {
|
|
370
375
|
let data = '';
|
|
371
376
|
resp.setEncoding('utf8');
|
package/lib/TuyaCloudDevice.js
CHANGED
|
@@ -76,6 +76,7 @@ class TuyaCloudDevice extends EventEmitter {
|
|
|
76
76
|
this._propertiesTried = new Set();
|
|
77
77
|
this._announcedProperties = false;
|
|
78
78
|
this._catchupTimers = null; // post-write state re-reads (see _scheduleStateCatchup)
|
|
79
|
+
this._writeChain = null; // serializes cloud writes to this device (see update)
|
|
79
80
|
|
|
80
81
|
// Bidirectional data-point maps, learned from the device's thing-shadow on
|
|
81
82
|
// connect. They let this cloud transport (and the LAN+cloud TuyaDevice that
|
|
@@ -214,7 +215,22 @@ class TuyaCloudDevice extends EventEmitter {
|
|
|
214
215
|
// first 'online' is caught. On any (re)connect we re-read the full state
|
|
215
216
|
// to recover anything missed while the stream was down.
|
|
216
217
|
this.messaging.on('online', () => this._refreshState());
|
|
217
|
-
this.messaging.subscribeDevice(this.context.id, status => this.
|
|
218
|
+
this.messaging.subscribeDevice(this.context.id, status => this._onRealtime(status));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// A realtime (MQTT) update arrived for this device, so it reports its changes
|
|
222
|
+
// over the stream and the post-write catch-up read is redundant — drop any
|
|
223
|
+
// pending one. A device whose reports never arrive over MQTT (some custom
|
|
224
|
+
// controllers — the stream carries nothing for them) gets none of these, so its
|
|
225
|
+
// catch-up stays armed and remains the only way its state reaches HomeKit. This
|
|
226
|
+
// makes the catch-up self-cancelling: it costs an HTTP read only on devices that
|
|
227
|
+
// actually need it.
|
|
228
|
+
_onRealtime(status) {
|
|
229
|
+
if (this._catchupTimers) {
|
|
230
|
+
this._catchupTimers.forEach(t => clearTimeout(t));
|
|
231
|
+
this._catchupTimers = null;
|
|
232
|
+
}
|
|
233
|
+
this._applyStatus(status);
|
|
218
234
|
}
|
|
219
235
|
|
|
220
236
|
async _refreshState() {
|
|
@@ -366,7 +382,16 @@ class TuyaCloudDevice extends EventEmitter {
|
|
|
366
382
|
if (undeliverable.length) this._reportUndeliverableWrite(undeliverable);
|
|
367
383
|
if (!sendable.length) return false;
|
|
368
384
|
|
|
369
|
-
|
|
385
|
+
// Serialize writes to this device. HomeKit frequently double-sends a
|
|
386
|
+
// characteristic write, and rapid changes (dragging a thermostat) fire a
|
|
387
|
+
// burst; running them concurrently both races the per-code endpoint
|
|
388
|
+
// learning (a duplicate could see a code "tried" but not yet learned and
|
|
389
|
+
// fail spuriously) and bursts the cloud's QPS limit (uneven latency). One
|
|
390
|
+
// at a time keeps learning coherent and the request rate smooth. _dispatch
|
|
391
|
+
// never rejects, so the chain can't break.
|
|
392
|
+
const run = () => this._dispatch(sendable);
|
|
393
|
+
this._writeChain = this._writeChain ? this._writeChain.then(run, run) : run();
|
|
394
|
+
return this._writeChain;
|
|
370
395
|
}
|
|
371
396
|
|
|
372
397
|
// Send a write, routing each data-point to the cloud endpoint it accepts. Codes
|
|
@@ -418,7 +443,7 @@ class TuyaCloudDevice extends EventEmitter {
|
|
|
418
443
|
}
|
|
419
444
|
if (!this._announcedProperties) {
|
|
420
445
|
this._announcedProperties = true;
|
|
421
|
-
this.log.
|
|
446
|
+
this.log.debug(`${this.context.name}: controlling some of this device's data-points over the Tuya thing-model property endpoint (they aren't in its standard instruction set).`);
|
|
422
447
|
}
|
|
423
448
|
return this._learn(fresh, 'properties');
|
|
424
449
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-tuya-plus",
|
|
3
|
-
"version": "3.14.0-dev.
|
|
3
|
+
"version": "3.14.0-dev.44",
|
|
4
4
|
"description": "A community-maintained Homebridge plugin for controlling Tuya devices in HomeKit. LAN-first, with an optional Tuya Cloud fallback for any device the LAN can't reach.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -240,6 +240,29 @@ describe('TuyaCloudDevice', () => {
|
|
|
240
240
|
expect(api.sendCommands).toHaveBeenCalledWith('dev1', [{code: 'switch_1', value: false}]);
|
|
241
241
|
expect(api.sendProperties).toHaveBeenCalledWith('dev1', [{code: 'wfh_open', value: true}]);
|
|
242
242
|
});
|
|
243
|
+
|
|
244
|
+
// HomeKit often double-sends a write; running the two concurrently used to
|
|
245
|
+
// race the learning (the 2nd saw the code "tried" but not yet learned and
|
|
246
|
+
// failed). Serializing writes per device fixes it.
|
|
247
|
+
test('concurrent duplicate writes of an unknown code are serialized, not raced', async () => {
|
|
248
|
+
const api = makeApi();
|
|
249
|
+
api.getShadowProperties = jest.fn().mockResolvedValue([{code: 'Power', dp_id: 1, value: false}]);
|
|
250
|
+
api.sendCommands.mockImplementation((id, cmds) =>
|
|
251
|
+
cmds.some(c => c.code === 'Power')
|
|
252
|
+
? Promise.reject(new Error('command or value not support (code 2008)'))
|
|
253
|
+
: Promise.resolve(true));
|
|
254
|
+
const dev = makeDevice(api, null, {key: 'abc'});
|
|
255
|
+
await dev._connect();
|
|
256
|
+
const warn = jest.spyOn(log, 'warn');
|
|
257
|
+
|
|
258
|
+
const [a, b] = await Promise.all([dev.update({'1': true}), dev.update({'1': true})]);
|
|
259
|
+
|
|
260
|
+
expect(a).toBe(true);
|
|
261
|
+
expect(b).toBe(true);
|
|
262
|
+
expect(warn).not.toHaveBeenCalled(); // the 2nd write didn't spuriously fail
|
|
263
|
+
expect(api.sendCommands).toHaveBeenCalledTimes(1); // only the 1st probed commands
|
|
264
|
+
jest.restoreAllMocks();
|
|
265
|
+
});
|
|
243
266
|
});
|
|
244
267
|
|
|
245
268
|
test('realtime code-only deltas are mirrored to numeric ids via the learned map', async () => {
|
|
@@ -319,6 +342,19 @@ describe('TuyaCloudDevice', () => {
|
|
|
319
342
|
dev.stop();
|
|
320
343
|
});
|
|
321
344
|
|
|
345
|
+
test('a realtime update cancels the pending catch-up (no redundant read on MQTT-covered devices)', async () => {
|
|
346
|
+
const api = makeApi();
|
|
347
|
+
api.getShadowProperties = jest.fn().mockResolvedValue([{code: 'switch_1', dp_id: 1, value: false}]);
|
|
348
|
+
const dev = makeDevice(api, null, {key: 'abc'});
|
|
349
|
+
await dev._connect();
|
|
350
|
+
await dev.update({'1': true});
|
|
351
|
+
expect(Array.isArray(dev._catchupTimers)).toBe(true);
|
|
352
|
+
|
|
353
|
+
dev._onRealtime([{code: 'switch_1', value: true}]); // MQTT delivers → catch-up not needed
|
|
354
|
+
expect(dev._catchupTimers).toBeNull();
|
|
355
|
+
dev.stop();
|
|
356
|
+
});
|
|
357
|
+
|
|
322
358
|
test('a refresh reads via the shadow, so a thing-model device whose /status is empty still updates', async () => {
|
|
323
359
|
const api = makeApi();
|
|
324
360
|
let rs = 13;
|