homebridge-tuya-plus 3.14.0-dev.44 → 3.14.0-dev.46
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/TuyaCloudDevice.js +28 -4
- package/package.json +1 -1
- package/test/TuyaCloudDevice.test.js +45 -0
package/lib/TuyaCloudDevice.js
CHANGED
|
@@ -77,6 +77,7 @@ class TuyaCloudDevice extends EventEmitter {
|
|
|
77
77
|
this._announcedProperties = false;
|
|
78
78
|
this._catchupTimers = null; // post-write state re-reads (see _scheduleStateCatchup)
|
|
79
79
|
this._writeChain = null; // serializes cloud writes to this device (see update)
|
|
80
|
+
this._reportsOverMqtt = false; // set once any realtime update arrives; disables the catch-up
|
|
80
81
|
|
|
81
82
|
// Bidirectional data-point maps, learned from the device's thing-shadow on
|
|
82
83
|
// connect. They let this cloud transport (and the LAN+cloud TuyaDevice that
|
|
@@ -226,6 +227,10 @@ class TuyaCloudDevice extends EventEmitter {
|
|
|
226
227
|
// makes the catch-up self-cancelling: it costs an HTTP read only on devices that
|
|
227
228
|
// actually need it.
|
|
228
229
|
_onRealtime(status) {
|
|
230
|
+
// This device reports over MQTT, so the catch-up is pure overhead for it —
|
|
231
|
+
// latch it off for good and drop any pending one. Only devices that never
|
|
232
|
+
// report here (the stream carries nothing for them) keep the catch-up.
|
|
233
|
+
this._reportsOverMqtt = true;
|
|
229
234
|
if (this._catchupTimers) {
|
|
230
235
|
this._catchupTimers.forEach(t => clearTimeout(t));
|
|
231
236
|
this._catchupTimers = null;
|
|
@@ -305,9 +310,26 @@ class TuyaCloudDevice extends EventEmitter {
|
|
|
305
310
|
_propsToState(props) {
|
|
306
311
|
const state = {};
|
|
307
312
|
(props || []).forEach(item => {
|
|
308
|
-
if (item
|
|
313
|
+
if (!item || typeof item !== 'object') return;
|
|
314
|
+
if (typeof item.code === 'string') {
|
|
309
315
|
this._indexInto(state, item.code, item.value, item.dp_id != null ? item.dp_id : item.dpId);
|
|
316
|
+
return;
|
|
310
317
|
}
|
|
318
|
+
// A realtime item with no `code`: Tuya's MQTT stream delivers some
|
|
319
|
+
// data-points as a bare { "<dpId>": value } pair — no `code`, no `value`
|
|
320
|
+
// field. Custom / non-standard DPs come through this way (e.g. an
|
|
321
|
+
// irrigation timer's `charging`, dp 101, which lives only in the
|
|
322
|
+
// thing-model shadow, not the standard instruction set, so the message
|
|
323
|
+
// service never attaches a code). Map the numeric id back to its code,
|
|
324
|
+
// learned from the shadow on connect, so the change is applied instead of
|
|
325
|
+
// silently dropped; a still-unmapped id is indexed numerically, which a
|
|
326
|
+
// LAN-style config resolves.
|
|
327
|
+
Object.keys(item).forEach(key => {
|
|
328
|
+
if (!/^\d+$/.test(key)) return;
|
|
329
|
+
const code = this.codeByDpId[key];
|
|
330
|
+
if (code != null) this._indexInto(state, code, item[key], key);
|
|
331
|
+
else state[key] = item[key];
|
|
332
|
+
});
|
|
311
333
|
});
|
|
312
334
|
return state;
|
|
313
335
|
}
|
|
@@ -452,10 +474,12 @@ class TuyaCloudDevice extends EventEmitter {
|
|
|
452
474
|
// to "opening") shows up in the cloud within ~a second, but the realtime stream
|
|
453
475
|
// doesn't always carry it — and an accessory that mirrors a status DP (the
|
|
454
476
|
// garage door) would otherwise sit on the old value forever. So re-read the
|
|
455
|
-
// device a couple of times after a command to catch the transition.
|
|
456
|
-
//
|
|
457
|
-
//
|
|
477
|
+
// device a couple of times after a command to catch the transition. Strictly
|
|
478
|
+
// supplemental: a device that has ever reported over MQTT skips this entirely
|
|
479
|
+
// (realtime is its path), each new command re-arms it, and it only runs on the
|
|
480
|
+
// cloud path — so a LAN-reachable or MQTT-covered device pays nothing.
|
|
458
481
|
_scheduleStateCatchup() {
|
|
482
|
+
if (this._reportsOverMqtt) return;
|
|
459
483
|
if (this._catchupTimers) this._catchupTimers.forEach(t => clearTimeout(t));
|
|
460
484
|
this._catchupTimers = [2000, 8000].map(delay => {
|
|
461
485
|
const t = setTimeout(() => this._catchupOnce(), delay);
|
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.46",
|
|
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": {
|
|
@@ -279,6 +279,39 @@ describe('TuyaCloudDevice', () => {
|
|
|
279
279
|
expect(changes[0]).toEqual({switch_1: true, '1': true});
|
|
280
280
|
});
|
|
281
281
|
|
|
282
|
+
test('realtime bare numeric-keyed items (no code) are mapped to their code', async () => {
|
|
283
|
+
// Custom/non-standard DPs (e.g. an irrigation timer's `charging`, dp 101)
|
|
284
|
+
// arrive over MQTT as a bare { "<dpId>": value } pair with no `code` field.
|
|
285
|
+
// They must be applied via the learned dp→code map, not dropped.
|
|
286
|
+
const api = makeApi();
|
|
287
|
+
api.getShadowProperties = jest.fn().mockResolvedValue([
|
|
288
|
+
{code: 'switch_1', dp_id: 1, value: false},
|
|
289
|
+
{code: 'charging', dp_id: 101, value: false}
|
|
290
|
+
]);
|
|
291
|
+
const dev = makeDevice(api);
|
|
292
|
+
await dev._connect();
|
|
293
|
+
|
|
294
|
+
const changes = [];
|
|
295
|
+
dev.on('change', c => changes.push(c));
|
|
296
|
+
dev._applyStatus([{'101': true}]); // bare numeric, as the message service delivers `charging`
|
|
297
|
+
expect(dev.state.charging).toBe(true);
|
|
298
|
+
expect(dev.state['101']).toBe(true);
|
|
299
|
+
expect(changes[0]).toEqual({charging: true, '101': true});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('a bare numeric item with no learned code is still indexed numerically', async () => {
|
|
303
|
+
const api = makeApi();
|
|
304
|
+
api.getShadowProperties = jest.fn().mockResolvedValue([{code: 'switch_1', dp_id: 1, value: false}]);
|
|
305
|
+
const dev = makeDevice(api);
|
|
306
|
+
await dev._connect();
|
|
307
|
+
|
|
308
|
+
const changes = [];
|
|
309
|
+
dev.on('change', c => changes.push(c));
|
|
310
|
+
dev._applyStatus([{'137': 42}]); // unknown dp, no code mapping
|
|
311
|
+
expect(dev.state['137']).toBe(42);
|
|
312
|
+
expect(changes[0]).toEqual({'137': 42});
|
|
313
|
+
});
|
|
314
|
+
|
|
282
315
|
test('falls back to /status (code-only) when the shadow is unavailable', async () => {
|
|
283
316
|
const api = makeApi();
|
|
284
317
|
api.getShadowProperties = jest.fn().mockResolvedValue(null);
|
|
@@ -355,6 +388,18 @@ describe('TuyaCloudDevice', () => {
|
|
|
355
388
|
dev.stop();
|
|
356
389
|
});
|
|
357
390
|
|
|
391
|
+
test('once a device has reported over MQTT, later writes never arm a catch-up', async () => {
|
|
392
|
+
const api = makeApi();
|
|
393
|
+
api.getShadowProperties = jest.fn().mockResolvedValue([{code: 'switch_1', dp_id: 1, value: false}]);
|
|
394
|
+
const dev = makeDevice(api, null, {key: 'abc'});
|
|
395
|
+
await dev._connect();
|
|
396
|
+
dev._onRealtime([{code: 'switch_1', value: true}]); // device reports over MQTT → latch
|
|
397
|
+
|
|
398
|
+
await dev.update({'1': false});
|
|
399
|
+
expect(dev._catchupTimers).toBeNull(); // strictly supplemental: no read for MQTT-covered devices
|
|
400
|
+
dev.stop();
|
|
401
|
+
});
|
|
402
|
+
|
|
358
403
|
test('a refresh reads via the shadow, so a thing-model device whose /status is empty still updates', async () => {
|
|
359
404
|
const api = makeApi();
|
|
360
405
|
let rs = 13;
|