homebridge-tuya-plus 3.14.0-dev.46 → 3.14.0-dev.48
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.
|
@@ -120,6 +120,11 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
120
120
|
? Characteristic.TargetDoorState.OPEN
|
|
121
121
|
: Characteristic.TargetDoorState.CLOSED;
|
|
122
122
|
this.accessory.context.cachedTargetDoorState = initialTarget;
|
|
123
|
+
// The level-triggered dispatch baseline (see setTargetDoorState). Held
|
|
124
|
+
// separately from cachedTargetDoorState because that one mirrors every
|
|
125
|
+
// reported value for HomeKit's benefit — including the transient "stopped"
|
|
126
|
+
// our own stop-before-close produces — which must not move this baseline.
|
|
127
|
+
this._committedTarget = initialTarget;
|
|
123
128
|
|
|
124
129
|
this.characteristicTargetDoorState = service.getCharacteristic(Characteristic.TargetDoorState)
|
|
125
130
|
.updateValue(initialTarget)
|
|
@@ -201,11 +206,24 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
201
206
|
// (=OPEN), which would fight the close we're about to send. Ignore
|
|
202
207
|
// status reports until the close has gone out.
|
|
203
208
|
if (this.pendingCloseTimer) return;
|
|
204
|
-
const
|
|
209
|
+
const raw = typeof changes[this.dpState] === 'string' ? parseInt(changes[this.dpState], 10) : changes[this.dpState];
|
|
210
|
+
const isOpen = this._mapDpState(raw);
|
|
205
211
|
if (isOpen === null) {
|
|
206
212
|
this.log.debug(`[SimpleGarageDoor] ${this.device.context.name}: ignoring unknown state DP value ${JSON.stringify(changes[this.dpState])}`);
|
|
207
213
|
return;
|
|
208
214
|
}
|
|
215
|
+
// Keep the level-triggered dispatch baseline honest with the gate's real
|
|
216
|
+
// position, but only from a DEFINITIVE report — 12 (opening/open) or 13
|
|
217
|
+
// (closing/closed). A bare 11 (stopped) is skipped: it is exactly what our
|
|
218
|
+
// own stop-before-close pulse makes the controller report, and over the LAN
|
|
219
|
+
// that report can land just after the stop→close window. Letting it flip
|
|
220
|
+
// the committed target back to OPEN is what let HomeKit's repeat close fire
|
|
221
|
+
// a second stop-before-close into the already-closing gate. A genuine
|
|
222
|
+
// external open always passes through 12 first, so nothing is missed.
|
|
223
|
+
if (raw === STATE_OPENING_OR_OPEN || raw === STATE_CLOSING_OR_CLOSED) {
|
|
224
|
+
const {Characteristic} = this.hap;
|
|
225
|
+
this._committedTarget = isOpen ? Characteristic.TargetDoorState.OPEN : Characteristic.TargetDoorState.CLOSED;
|
|
226
|
+
}
|
|
209
227
|
this._applyReportedState(isOpen);
|
|
210
228
|
}
|
|
211
229
|
|
|
@@ -257,6 +275,22 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
257
275
|
// the partial switch tapped OFF) is manual control: cancel any pending
|
|
258
276
|
// partial-open auto-stop so it can't halt this movement part-way.
|
|
259
277
|
this._cancelPartialStop();
|
|
278
|
+
|
|
279
|
+
// The opener is level-triggered: act only when the requested target
|
|
280
|
+
// differs from the one we've already committed to. HomeKit re-sends the
|
|
281
|
+
// same target after a tap — a second controller echoing it, or its own
|
|
282
|
+
// retry a few seconds later — and acting on that repeat would fire another
|
|
283
|
+
// open/close. For a close that means a second stop-before-close into the
|
|
284
|
+
// already-closing gate, halting it mid-travel and restarting it (the
|
|
285
|
+
// reported stutter). A genuine request always flips the target (the Home
|
|
286
|
+
// app toggles to the opposite state). The committed target is reconciled
|
|
287
|
+
// with the gate's real position in _onDeviceChange so external operation
|
|
288
|
+
// still works — but only from a definitive report, never from the
|
|
289
|
+
// "stopped" transient our own stop produces, so the repeat stays caught.
|
|
290
|
+
if (value === this._committedTarget) {
|
|
291
|
+
this.log.debug(`[SimpleGarageDoor] ${this.device.context.name}: target unchanged — ignoring repeat request`);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
260
294
|
this._applyTarget(value);
|
|
261
295
|
}
|
|
262
296
|
|
|
@@ -267,6 +301,7 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
267
301
|
_applyTarget(value) {
|
|
268
302
|
const {Characteristic} = this.hap;
|
|
269
303
|
const open = value === Characteristic.TargetDoorState.OPEN;
|
|
304
|
+
this._committedTarget = value;
|
|
270
305
|
this.accessory.context.cachedTargetDoorState = value;
|
|
271
306
|
if (this.characteristicTargetDoorState) this.characteristicTargetDoorState.updateValue(value);
|
|
272
307
|
if (open) this._sendOpen();
|
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.48",
|
|
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": {
|
|
@@ -68,6 +68,7 @@ function makeSimpleGarage(initialContext = {}) {
|
|
|
68
68
|
updateValue: jest.fn().mockImplementation(function(v) { this.value = v; return this; }),
|
|
69
69
|
};
|
|
70
70
|
accessory.context.cachedTargetDoorState = TDS.CLOSED;
|
|
71
|
+
instance._committedTarget = TDS.CLOSED;
|
|
71
72
|
|
|
72
73
|
// Mirror the persistent change listener registered in production.
|
|
73
74
|
device.on('change', changes => instance._onDeviceChange(changes));
|
|
@@ -226,9 +227,19 @@ describe('SimpleGarageDoorAccessory.setTargetDoorState — close (stop-before-cl
|
|
|
226
227
|
beforeEach(() => jest.useFakeTimers());
|
|
227
228
|
afterEach(() => jest.useRealTimers());
|
|
228
229
|
|
|
230
|
+
// A close is only ever issued from an open gate, so the committed target
|
|
231
|
+
// (cachedTargetDoorState) starts OPEN — the state HomeKit shows before the
|
|
232
|
+
// tap. Without it the very first close would match the helper's default
|
|
233
|
+
// committed target and be (correctly) treated as a redundant repeat.
|
|
234
|
+
function openGate(instance, device, state = STATE_OPENING_OR_OPEN) {
|
|
235
|
+
if (state !== undefined) device.state['105'] = state;
|
|
236
|
+
instance.accessory.context.cachedTargetDoorState = TDS.OPEN;
|
|
237
|
+
instance._committedTarget = TDS.OPEN;
|
|
238
|
+
}
|
|
239
|
+
|
|
229
240
|
test('CLOSE fires immediately when the gate is already stopped (state 11)', () => {
|
|
230
241
|
const { instance, device, accessory } = makeSimpleGarage();
|
|
231
|
-
device
|
|
242
|
+
openGate(instance, device, STATE_STOPPED);
|
|
232
243
|
|
|
233
244
|
instance.setTargetDoorState(TDS.CLOSED);
|
|
234
245
|
|
|
@@ -242,7 +253,7 @@ describe('SimpleGarageDoorAccessory.setTargetDoorState — close (stop-before-cl
|
|
|
242
253
|
test('CLOSE while opening (state 12) fires stop, then close after stopBeforeCloseMs', () => {
|
|
243
254
|
const { instance, device } = makeSimpleGarage();
|
|
244
255
|
instance.stopBeforeCloseMs = 1500;
|
|
245
|
-
device
|
|
256
|
+
openGate(instance, device, STATE_OPENING_OR_OPEN);
|
|
246
257
|
|
|
247
258
|
instance.setTargetDoorState(TDS.CLOSED);
|
|
248
259
|
|
|
@@ -259,10 +270,10 @@ describe('SimpleGarageDoorAccessory.setTargetDoorState — close (stop-before-cl
|
|
|
259
270
|
expect(instance.pendingCloseTimer).toBeNull();
|
|
260
271
|
});
|
|
261
272
|
|
|
262
|
-
test('CLOSE
|
|
273
|
+
test('CLOSE that reads state 13 still uses stop-before-close (13 is not "stopped")', () => {
|
|
263
274
|
const { instance, device } = makeSimpleGarage();
|
|
264
275
|
instance.stopBeforeCloseMs = 1500;
|
|
265
|
-
device
|
|
276
|
+
openGate(instance, device, STATE_CLOSING_OR_CLOSED);
|
|
266
277
|
|
|
267
278
|
instance.setTargetDoorState(TDS.CLOSED);
|
|
268
279
|
expect(device.update).toHaveBeenNthCalledWith(1, { '103': true });
|
|
@@ -274,7 +285,7 @@ describe('SimpleGarageDoorAccessory.setTargetDoorState — close (stop-before-cl
|
|
|
274
285
|
test('CLOSE with no reported state yet uses stop-before-close', () => {
|
|
275
286
|
const { instance, device } = makeSimpleGarage();
|
|
276
287
|
instance.stopBeforeCloseMs = 1500;
|
|
277
|
-
// device.state['105']
|
|
288
|
+
openGate(instance, device, undefined); // device.state['105'] left undefined
|
|
278
289
|
|
|
279
290
|
instance.setTargetDoorState(TDS.CLOSED);
|
|
280
291
|
expect(device.update).toHaveBeenNthCalledWith(1, { '103': true });
|
|
@@ -286,7 +297,7 @@ describe('SimpleGarageDoorAccessory.setTargetDoorState — close (stop-before-cl
|
|
|
286
297
|
test('A duplicate CLOSE during the wait does not restart or double-fire', () => {
|
|
287
298
|
const { instance, device } = makeSimpleGarage();
|
|
288
299
|
instance.stopBeforeCloseMs = 1500;
|
|
289
|
-
device
|
|
300
|
+
openGate(instance, device, STATE_OPENING_OR_OPEN);
|
|
290
301
|
|
|
291
302
|
instance.setTargetDoorState(TDS.CLOSED);
|
|
292
303
|
expect(device.update).toHaveBeenCalledTimes(1); // stop
|
|
@@ -300,10 +311,71 @@ describe('SimpleGarageDoorAccessory.setTargetDoorState — close (stop-before-cl
|
|
|
300
311
|
expect(device.update).toHaveBeenNthCalledWith(2, { '102': true });
|
|
301
312
|
});
|
|
302
313
|
|
|
314
|
+
test('A repeat close once committed is swallowed — no second stop-before-close into the moving gate', () => {
|
|
315
|
+
// HomeKit re-sends the same target seconds after a tap (a second
|
|
316
|
+
// controller echoing it, or its own retry). Acting on it fired another
|
|
317
|
+
// stop-before-close that halted the closing gate and restarted it — the
|
|
318
|
+
// reported stutter. Level-triggered dispatch makes the repeat a no-op.
|
|
319
|
+
const { instance, device } = makeSimpleGarage();
|
|
320
|
+
instance.stopBeforeCloseMs = 1500;
|
|
321
|
+
openGate(instance, device, STATE_OPENING_OR_OPEN);
|
|
322
|
+
|
|
323
|
+
instance.setTargetDoorState(TDS.CLOSED);
|
|
324
|
+
jest.advanceTimersByTime(1500);
|
|
325
|
+
expect(device.update).toHaveBeenCalledTimes(2); // stop + close
|
|
326
|
+
expect(device.update).toHaveBeenNthCalledWith(2, { '102': true });
|
|
327
|
+
|
|
328
|
+
// The gate is now committed closed and still travelling (reports 13).
|
|
329
|
+
emitState(device, STATE_CLOSING_OR_CLOSED);
|
|
330
|
+
jest.advanceTimersByTime(3000);
|
|
331
|
+
instance.setTargetDoorState(TDS.CLOSED); // HomeKit's repeat close
|
|
332
|
+
expect(device.update).toHaveBeenCalledTimes(2); // swallowed — no second stop/close
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test('A stopped (11) report after the close does not re-enable the repeat (LAN stutter)', () => {
|
|
336
|
+
// Over the LAN the stop pulse makes the controller report 11 (=OPEN) just
|
|
337
|
+
// after the close has gone out — outside the stop-before-close window. That
|
|
338
|
+
// 11 must NOT move the committed target back to OPEN, or HomeKit's repeat
|
|
339
|
+
// close would fire a second stop-before-close into the already-closing gate.
|
|
340
|
+
const { instance, device } = makeSimpleGarage();
|
|
341
|
+
instance.stopBeforeCloseMs = 1500;
|
|
342
|
+
openGate(instance, device, STATE_OPENING_OR_OPEN);
|
|
343
|
+
|
|
344
|
+
instance.setTargetDoorState(TDS.CLOSED);
|
|
345
|
+
jest.advanceTimersByTime(1500);
|
|
346
|
+
expect(device.update).toHaveBeenCalledTimes(2); // stop + close
|
|
347
|
+
|
|
348
|
+
emitState(device, STATE_STOPPED); // the stop's late 11 report
|
|
349
|
+
expect(instance._committedTarget).toBe(TDS.CLOSED); // not bounced to OPEN
|
|
350
|
+
instance.setTargetDoorState(TDS.CLOSED); // HomeKit's repeat
|
|
351
|
+
expect(device.update).toHaveBeenCalledTimes(2); // swallowed
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('A close runs again once the gate is reported open (committed target tracks reports)', () => {
|
|
355
|
+
const { instance, device } = makeSimpleGarage();
|
|
356
|
+
instance.stopBeforeCloseMs = 1500;
|
|
357
|
+
openGate(instance, device, STATE_OPENING_OR_OPEN);
|
|
358
|
+
|
|
359
|
+
instance.setTargetDoorState(TDS.CLOSED);
|
|
360
|
+
jest.advanceTimersByTime(1500);
|
|
361
|
+
expect(device.update).toHaveBeenCalledTimes(2); // stop + close
|
|
362
|
+
emitState(device, STATE_CLOSING_OR_CLOSED); // committed = closed
|
|
363
|
+
|
|
364
|
+
instance.setTargetDoorState(TDS.CLOSED); // repeat — swallowed
|
|
365
|
+
expect(device.update).toHaveBeenCalledTimes(2);
|
|
366
|
+
|
|
367
|
+
// The gate is opened again (here, externally) — a close is a real
|
|
368
|
+
// transition once more and runs.
|
|
369
|
+
emitState(device, STATE_OPENING_OR_OPEN); // committed = open
|
|
370
|
+
instance.setTargetDoorState(TDS.CLOSED);
|
|
371
|
+
expect(device.update).toHaveBeenCalledTimes(3);
|
|
372
|
+
expect(device.update).toHaveBeenNthCalledWith(3, { '103': true });
|
|
373
|
+
});
|
|
374
|
+
|
|
303
375
|
test('OPEN during the wait cancels the pending close and opens instead', () => {
|
|
304
376
|
const { instance, device } = makeSimpleGarage();
|
|
305
377
|
instance.stopBeforeCloseMs = 1500;
|
|
306
|
-
device
|
|
378
|
+
openGate(instance, device, STATE_OPENING_OR_OPEN);
|
|
307
379
|
|
|
308
380
|
instance.setTargetDoorState(TDS.CLOSED);
|
|
309
381
|
expect(device.update).toHaveBeenNthCalledWith(1, { '103': true }); // stop
|
|
@@ -321,7 +393,7 @@ describe('SimpleGarageDoorAccessory.setTargetDoorState — close (stop-before-cl
|
|
|
321
393
|
test('Status reports are ignored during the stop-before-close window', () => {
|
|
322
394
|
const { instance, device, accessory } = makeSimpleGarage();
|
|
323
395
|
instance.stopBeforeCloseMs = 1500;
|
|
324
|
-
device
|
|
396
|
+
openGate(instance, device, STATE_OPENING_OR_OPEN);
|
|
325
397
|
instance.currentDoorState = CDS.OPEN;
|
|
326
398
|
instance.characteristicCurrentDoorState.value = CDS.OPEN;
|
|
327
399
|
|
|
@@ -345,7 +417,7 @@ describe('SimpleGarageDoorAccessory.setTargetDoorState — close (stop-before-cl
|
|
|
345
417
|
test('stopBeforeCloseMs is configurable', () => {
|
|
346
418
|
const { instance, device } = makeSimpleGarage();
|
|
347
419
|
instance.stopBeforeCloseMs = 300;
|
|
348
|
-
device
|
|
420
|
+
openGate(instance, device, STATE_OPENING_OR_OPEN);
|
|
349
421
|
|
|
350
422
|
instance.setTargetDoorState(TDS.CLOSED);
|
|
351
423
|
expect(device.update).toHaveBeenNthCalledWith(1, { '103': true });
|
|
@@ -361,7 +433,7 @@ describe('SimpleGarageDoorAccessory.setTargetDoorState — close (stop-before-cl
|
|
|
361
433
|
instance.dpClose = '2';
|
|
362
434
|
instance.dpStop = '4';
|
|
363
435
|
instance.stopBeforeCloseMs = 1000;
|
|
364
|
-
device
|
|
436
|
+
openGate(instance, device, STATE_OPENING_OR_OPEN);
|
|
365
437
|
|
|
366
438
|
instance.setTargetDoorState(TDS.CLOSED);
|
|
367
439
|
expect(device.update).toHaveBeenNthCalledWith(1, { '4': true });
|
|
@@ -580,6 +652,7 @@ describe('SimpleGarageDoorAccessory — force switches', () => {
|
|
|
580
652
|
test('Force Close fires the close action (immediately when already stopped)', () => {
|
|
581
653
|
const { instance, device, accessory } = makeSimpleGarage();
|
|
582
654
|
device.state['105'] = STATE_STOPPED;
|
|
655
|
+
instance._committedTarget = TDS.OPEN; // gate open → close is a real transition
|
|
583
656
|
|
|
584
657
|
instance.setTargetDoorState(TDS.CLOSED);
|
|
585
658
|
|